diff --git a/package-lock.json b/package-lock.json index 827685ed..a5ea9c8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "@testing-library/jest-dom": "^6.8.0", "@testing-library/svelte": "^5.1.0", "@testing-library/user-event": "^14.6.1", - "@thwbh/veilchen": "^0.2.2", + "@thwbh/veilchen": "^0.2.3", "@tsconfig/svelte": "^5.0.4", "@types/date-fns": "^2.6.0", "@types/node": "^22.10.1", @@ -65,7 +65,7 @@ }, "../veilchen": { "name": "@thwbh/veilchen", - "version": "0.2.2", + "version": "0.2.3", "extraneous": true, "license": "MIT", "devDependencies": { @@ -2074,9 +2074,9 @@ } }, "node_modules/@thwbh/veilchen": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@thwbh/veilchen/-/veilchen-0.2.2.tgz", - "integrity": "sha512-FabUTDYeyObacJso33V42/v3/LJ/qRHqJ2O77t/W1jb9rfYj/0+DslY8fwf8DxrutHxiq0YAZie9dBXwEI9VTg==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@thwbh/veilchen/-/veilchen-0.2.3.tgz", + "integrity": "sha512-T3sSbK3SJRnVMFtfo5KqLcot1lv1tNmRCpxmGYnyotrMXFocK021HEpXP4BmPmgDfMdfTNVQ9YB9P2S/41r4JQ==", "dev": true, "license": "MIT", "peerDependencies": { diff --git a/package.json b/package.json index 38ece7fd..b689cb89 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@testing-library/jest-dom": "^6.8.0", "@testing-library/svelte": "^5.1.0", "@testing-library/user-event": "^14.6.1", - "@thwbh/veilchen": "^0.2.2", + "@thwbh/veilchen": "^0.2.3", "@tsconfig/svelte": "^5.0.4", "@types/date-fns": "^2.6.0", "@types/node": "^22.10.1", diff --git a/src-tauri/src/service/dashboard.rs b/src-tauri/src/service/dashboard.rs index 2591fdf9..f6065a2a 100644 --- a/src-tauri/src/service/dashboard.rs +++ b/src-tauri/src/service/dashboard.rs @@ -24,6 +24,8 @@ pub struct Dashboard { pub weight_month_list: Vec, pub food_categories: Vec, pub current_day: i32, + pub days_total: i32, + pub weight_latest: WeightTracker, } // ============================================================================ @@ -88,6 +90,13 @@ impl Dashboard { .num_days() as i32; let current_day: i32 = day_count + 1; // Day count will be zero at the first day + let days_total: i32 = intake_target_end_date + .signed_duration_since(intake_target_start_date) + .num_days() as i32; + + let weight_latest = + WeightTracker::get_latest(conn).map_err(|_| "No weight tracker found".to_string())?; + Ok(Self { user_data, intake_target, @@ -98,6 +107,8 @@ impl Dashboard { weight_month_list, food_categories, current_day, + days_total, + weight_latest, }) } } diff --git a/src-tauri/src/service/progress.rs b/src-tauri/src/service/progress.rs index 3a3f389f..fec5a813 100644 --- a/src-tauri/src/service/progress.rs +++ b/src-tauri/src/service/progress.rs @@ -169,7 +169,7 @@ fn process_intake( .into_iter() .map(|(category, (sum, count))| { FoodCategory::find_by_key(conn, category) - .map(|cat| (cat.longvalue, math_f32::floor_f32(sum / count as f32, 0))) + .map(|cat| (cat.shortvalue, math_f32::floor_f32(sum / count as f32, 0))) .map_err(|e| format!("Failed to get food category: {}", e)) }) .collect::, String>>()?; diff --git a/src-tauri/tests/cmd/test_dashboard_cmd.rs b/src-tauri/tests/cmd/test_dashboard_cmd.rs index dae81a02..c701227b 100644 --- a/src-tauri/tests/cmd/test_dashboard_cmd.rs +++ b/src-tauri/tests/cmd/test_dashboard_cmd.rs @@ -2,7 +2,7 @@ use crate::helpers::{ create_test_intake_entry, create_test_intake_target, create_test_user, create_test_weight_entry, create_test_weight_target, setup_test_pool, }; -use librefit_lib::service::dashboard::daily_dashboard; +use librefit_lib::service::{dashboard::daily_dashboard, weight::create_weight_tracker_entry}; use tauri::Manager; #[test] @@ -61,6 +61,7 @@ fn test_daily_dashboard_first_day() { create_test_user(&pool, "User", "avatar.png"); create_test_intake_target(&pool, "2025-01-01", "2025-06-01", 2000, 2500); create_test_weight_target(&pool, "2025-01-01", "2025-06-01", 85.0, 75.0); + create_test_weight_entry(&pool, "2025-01-01", 85.0); let app = tauri::test::mock_app(); app.manage(pool); @@ -73,7 +74,7 @@ fn test_daily_dashboard_first_day() { // First day should be day 0 (0 days completed on day 1) assert_eq!(dashboard.current_day, 0); assert_eq!(dashboard.intake_today_list.len(), 0); - assert_eq!(dashboard.weight_today_list.len(), 0); + assert_eq!(dashboard.weight_today_list.len(), 1); } #[test] @@ -84,6 +85,8 @@ fn test_daily_dashboard_last_day_of_target() { create_test_intake_target(&pool, "2025-01-01", "2025-01-31", 2000, 2500); create_test_weight_target(&pool, "2025-01-01", "2025-01-31", 85.0, 75.0); + create_test_weight_entry(&pool, "2025-01-31", 75.0); + let app = tauri::test::mock_app(); app.manage(pool); @@ -110,6 +113,8 @@ fn test_daily_dashboard_date_beyond_target() { create_test_intake_entry(&pool, "2025-01-27", 2000, "l", None); create_test_intake_entry(&pool, "2025-01-31", 2000, "l", None); + create_test_weight_entry(&pool, "2025-01-31", 85.0); + let app = tauri::test::mock_app(); app.manage(pool); @@ -135,6 +140,7 @@ fn test_daily_dashboard_with_week_data() { // Add entries over the past week for day in 8..=15 { create_test_intake_entry(&pool, &format!("2025-01-{:02}", day), 1800, "l", None); + create_test_weight_entry(&pool, &format!("2025-01-{:02}", day), 85.4); } let app = tauri::test::mock_app(); @@ -147,6 +153,7 @@ fn test_daily_dashboard_with_week_data() { // Should have 8 days of data (Jan 8-15) assert_eq!(dashboard.intake_week_list.len(), 8); + assert_eq!(dashboard.weight_month_list.len(), 8); } #[test] @@ -247,14 +254,7 @@ fn test_daily_dashboard_empty_trackers() { let result = daily_dashboard(app.state(), "2025-01-15".to_string()); - assert!(result.is_ok()); - let dashboard = result.unwrap(); - - // Should succeed with empty lists - assert_eq!(dashboard.intake_today_list.len(), 0); - assert_eq!(dashboard.intake_week_list.len(), 0); - assert_eq!(dashboard.weight_today_list.len(), 0); - assert_eq!(dashboard.weight_month_list.len(), 0); + assert!(result.is_err()); } #[test] @@ -272,6 +272,8 @@ fn test_daily_dashboard_multiple_categories() { create_test_intake_entry(&pool, "2025-01-15", 200, "s", Some("Snack".to_string())); create_test_intake_entry(&pool, "2025-01-15", 100, "t", Some("Treat".to_string())); + create_test_weight_entry(&pool, "2025-01-15", 85.0); + let app = tauri::test::mock_app(); app.manage(pool); diff --git a/src/lib/activity.ts b/src/lib/activity.ts index 576d0ebe..01eedb1a 100644 --- a/src/lib/activity.ts +++ b/src/lib/activity.ts @@ -1,14 +1,12 @@ import { Barbell, OfficeChair, PersonSimpleRun, PersonSimpleTaiChi, Trophy } from 'phosphor-svelte'; import type { Component } from 'svelte'; +import { BadgeColor, type OptionCardBadge } from '@thwbh/veilchen'; export interface ActivityLevelInfo { level: number; label: string; icon: Component; - badge: { - text: string; - color: 'success' | 'error' | 'warning' | 'info' | 'primary' | 'secondary' | 'accent'; - }; + badge: OptionCardBadge; description: string; } @@ -17,7 +15,7 @@ export const activityLevels: ActivityLevelInfo[] = [ level: 1, label: 'Mostly Sedentary', icon: OfficeChair, - badge: { text: 'Level 1', color: 'secondary' }, + badge: { text: 'Level 1', color: BadgeColor.Secondary }, description: 'You likely have an office job and try your best reaching your daily step goal. Apart from that, you do not work out regularly and spend most of your day stationary.' }, @@ -25,7 +23,7 @@ export const activityLevels: ActivityLevelInfo[] = [ level: 1.25, label: 'Light Activity', icon: PersonSimpleTaiChi, - badge: { text: 'Level 2', color: 'secondary' }, + badge: { text: 'Level 2', color: BadgeColor.Secondary }, description: 'You either have a job that requires you to move around frequently or you hit the gym 2x - 3x times a week. In either way, you are regularly lifting weight and training your cardiovascular system.' }, @@ -33,7 +31,7 @@ export const activityLevels: ActivityLevelInfo[] = [ level: 1.5, label: 'Moderate Activity', icon: PersonSimpleRun, - badge: { text: 'Level 3', color: 'secondary' }, + badge: { text: 'Level 3', color: BadgeColor.Secondary }, description: 'You consistently train your body 3x - 4x times a week. Your training plan became more sophisticated over the years and include cardiovascular HIIT sessions. You realized how important nutrition is and want to improve your sportive results.' }, @@ -41,7 +39,7 @@ export const activityLevels: ActivityLevelInfo[] = [ level: 1.75, label: 'Highly Active', icon: Barbell, - badge: { text: 'Level 4', color: 'accent' }, + badge: { text: 'Level 4', color: BadgeColor.Warning }, description: 'Fitness is your top priority in life. You dedicate large parts of your week to train your body, maybe even regularly visit sportive events. You work out almost every day and certainly know what you are doing.' }, @@ -49,7 +47,7 @@ export const activityLevels: ActivityLevelInfo[] = [ level: 2, label: 'Athlete', icon: Trophy, - badge: { text: 'Level 5', color: 'warning' }, + badge: { text: 'Level 5', color: BadgeColor.Accent }, description: "Your fitness level reaches into the (semi-) professional realm. Calculators like this won't fulfill your needs and you are curious how far off the results will be." } diff --git a/src/lib/api/category.ts b/src/lib/api/category.ts index 7f0f503d..3a218ed9 100644 --- a/src/lib/api/category.ts +++ b/src/lib/api/category.ts @@ -1,20 +1,23 @@ import type { FoodCategory } from '$lib/api/gen'; +import { BowlFood, Coffee, Cookie, ForkKnife, IceCream, PintGlass } from 'phosphor-svelte'; +import type { Component } from 'svelte'; +const categoryIcons: Record = { + b: Coffee, + l: BowlFood, + d: ForkKnife, + s: Cookie, + t: IceCream, + u: PintGlass +}; -const categoryColors = new Map([ - ['b', 'bg-warning'], // Breakfast → #f5b474 (warm orange) - ['l', 'bg-success'], // Lunch → #63ca7b (fresh green) - ['d', 'bg-primary'], // Dinner → oklch(52% 0.105 223) (cool blue) - ['s', 'bg-secondary'], // Snack → oklch(70% 0.183 293) (purple) - ['t', 'bg-accent'] // Treat → oklch(82% 0.119 306) (pink/lavender) -]); export const getFoodCategoryLongvalue = ( - foodCategories: Array, - shortvalue: string + foodCategories: Array, + shortvalue: string ): string => { - return foodCategories.filter((fc) => fc.shortvalue === shortvalue)[0].longvalue; + return foodCategories.filter((fc) => fc.shortvalue === shortvalue)[0].longvalue; }; -export const getFoodCategoryColor = (shortvalue: string): string => { - return categoryColors.get(shortvalue)!; -} +export const getFoodCategoryIcon = (shortvalue: string): Component => { + return categoryIcons[shortvalue]; +}; diff --git a/src/lib/assets/icons/alert-circle-filled.svg b/src/lib/assets/icons/alert-circle-filled.svg deleted file mode 100644 index 56726308..00000000 --- a/src/lib/assets/icons/alert-circle-filled.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/lib/assets/icons/arrow-right.svg b/src/lib/assets/icons/arrow-right.svg deleted file mode 100644 index 8248e0b3..00000000 --- a/src/lib/assets/icons/arrow-right.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/book-bookmark.svg b/src/lib/assets/icons/book-bookmark.svg new file mode 100644 index 00000000..bab00906 --- /dev/null +++ b/src/lib/assets/icons/book-bookmark.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + diff --git a/src/lib/assets/icons/bookmark.svg b/src/lib/assets/icons/bookmark.svg new file mode 100644 index 00000000..cc23ec88 --- /dev/null +++ b/src/lib/assets/icons/bookmark.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + diff --git a/src/lib/assets/icons/chart-line.svg b/src/lib/assets/icons/chart-line.svg deleted file mode 100644 index 0a07ae6d..00000000 --- a/src/lib/assets/icons/chart-line.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/chart-pie-4.svg b/src/lib/assets/icons/chart-pie-4.svg deleted file mode 100644 index 4a4d67cf..00000000 --- a/src/lib/assets/icons/chart-pie-4.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/check.svg b/src/lib/assets/icons/check.svg deleted file mode 100644 index ee6bdf61..00000000 --- a/src/lib/assets/icons/check.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/circle-check.svg b/src/lib/assets/icons/circle-check.svg deleted file mode 100644 index 04d9fec4..00000000 --- a/src/lib/assets/icons/circle-check.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/circle-x.svg b/src/lib/assets/icons/circle-x.svg deleted file mode 100644 index ffe1b41b..00000000 --- a/src/lib/assets/icons/circle-x.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/dashboard.svg b/src/lib/assets/icons/dashboard.svg deleted file mode 100644 index 48b1d496..00000000 --- a/src/lib/assets/icons/dashboard.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/file-type-csv.svg b/src/lib/assets/icons/file-type-csv.svg deleted file mode 100644 index 078ce99c..00000000 --- a/src/lib/assets/icons/file-type-csv.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/file-upload.svg b/src/lib/assets/icons/file-upload.svg deleted file mode 100644 index f23afc37..00000000 --- a/src/lib/assets/icons/file-upload.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/fit.svg b/src/lib/assets/icons/fit.svg new file mode 100644 index 00000000..56d47611 --- /dev/null +++ b/src/lib/assets/icons/fit.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/lib/assets/icons/food-off.svg b/src/lib/assets/icons/food-off.svg deleted file mode 100644 index b68595c7..00000000 --- a/src/lib/assets/icons/food-off.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/food.svg b/src/lib/assets/icons/food.svg deleted file mode 100644 index 3141e998..00000000 --- a/src/lib/assets/icons/food.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/github.svg b/src/lib/assets/icons/github.svg deleted file mode 100644 index 14c8bc4c..00000000 --- a/src/lib/assets/icons/github.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/hamburger-plus.svg b/src/lib/assets/icons/hamburger-plus.svg deleted file mode 100644 index 9e4d710e..00000000 --- a/src/lib/assets/icons/hamburger-plus.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/history.svg b/src/lib/assets/icons/history.svg deleted file mode 100644 index 6450c8ef..00000000 --- a/src/lib/assets/icons/history.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/journal.svg b/src/lib/assets/icons/journal.svg new file mode 100644 index 00000000..4a535bf4 --- /dev/null +++ b/src/lib/assets/icons/journal.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + diff --git a/src/lib/assets/icons/list-bullets.png b/src/lib/assets/icons/list-bullets.png new file mode 100644 index 00000000..b7d4bf44 Binary files /dev/null and b/src/lib/assets/icons/list-bullets.png differ diff --git a/src/lib/assets/icons/list-bullets.svg b/src/lib/assets/icons/list-bullets.svg new file mode 100644 index 00000000..30a46689 --- /dev/null +++ b/src/lib/assets/icons/list-bullets.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/lib/assets/icons/login.svg b/src/lib/assets/icons/login.svg deleted file mode 100644 index 1d9e9b86..00000000 --- a/src/lib/assets/icons/login.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/new-section.svg b/src/lib/assets/icons/new-section.svg deleted file mode 100644 index 3e0714d8..00000000 --- a/src/lib/assets/icons/new-section.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/notebook.svg b/src/lib/assets/icons/notebook.svg deleted file mode 100644 index 77591edf..00000000 --- a/src/lib/assets/icons/notebook.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/overflow-1.svg b/src/lib/assets/icons/overflow-1.svg deleted file mode 100644 index ec40c8e7..00000000 --- a/src/lib/assets/icons/overflow-1.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/overflow-2.svg b/src/lib/assets/icons/overflow-2.svg deleted file mode 100644 index 3eb238b0..00000000 --- a/src/lib/assets/icons/overflow-2.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/pencil-off.svg b/src/lib/assets/icons/pencil-off.svg deleted file mode 100644 index 20fadef8..00000000 --- a/src/lib/assets/icons/pencil-off.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/pencil.svg b/src/lib/assets/icons/pencil.svg deleted file mode 100644 index dfe2334c..00000000 --- a/src/lib/assets/icons/pencil.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/plus.svg b/src/lib/assets/icons/plus.svg deleted file mode 100644 index d7e5b7a8..00000000 --- a/src/lib/assets/icons/plus.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/progress.svg b/src/lib/assets/icons/progress.svg new file mode 100644 index 00000000..63132305 --- /dev/null +++ b/src/lib/assets/icons/progress.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib/assets/icons/scale-outline-off.svg b/src/lib/assets/icons/scale-outline-off.svg deleted file mode 100644 index bfbce6fe..00000000 --- a/src/lib/assets/icons/scale-outline-off.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/scale-outline.svg b/src/lib/assets/icons/scale-outline.svg deleted file mode 100644 index 22aae270..00000000 --- a/src/lib/assets/icons/scale-outline.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/target-arrow.svg b/src/lib/assets/icons/target-arrow.svg deleted file mode 100644 index 54b58efa..00000000 --- a/src/lib/assets/icons/target-arrow.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/target-off.svg b/src/lib/assets/icons/target-off.svg deleted file mode 100644 index 61f4190f..00000000 --- a/src/lib/assets/icons/target-off.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/trash-off.svg b/src/lib/assets/icons/trash-off.svg deleted file mode 100644 index 9942ab4e..00000000 --- a/src/lib/assets/icons/trash-off.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/trash.svg b/src/lib/assets/icons/trash.svg deleted file mode 100644 index d872bd7a..00000000 --- a/src/lib/assets/icons/trash.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/user-off.svg b/src/lib/assets/icons/user-off.svg deleted file mode 100644 index aec4e0ce..00000000 --- a/src/lib/assets/icons/user-off.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/user-square-rounded.svg b/src/lib/assets/icons/user-square-rounded.svg deleted file mode 100644 index 3807b207..00000000 --- a/src/lib/assets/icons/user-square-rounded.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/user.svg b/src/lib/assets/icons/user.svg deleted file mode 100644 index 1e828549..00000000 --- a/src/lib/assets/icons/user.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/assets/icons/wand.svg b/src/lib/assets/icons/wand.svg deleted file mode 100644 index e86b6762..00000000 --- a/src/lib/assets/icons/wand.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/component/intake/IntakeMask.svelte b/src/lib/component/intake/IntakeMask.svelte index 5eae0921..69651dcd 100644 --- a/src/lib/component/intake/IntakeMask.svelte +++ b/src/lib/component/intake/IntakeMask.svelte @@ -1,5 +1,5 @@ -
-

Calorie Plan

-
- {#if !isHolding || dailyRate !== 0} -
- {rateLabel} - {dailyRate} kcal -
- {/if} -
- {targetLabel} - {targetCalories} kcal +
+
+

Calorie Plan

+ {goalLabel} +
+ + +
+ + + + kcal / day +
+ + +
+ +
+ Target + {targetCalories} kcal
-
- {maximumLabel} - {maximumCalories} kcal + + +
+ +
+
+
+ + + {#if showAverage} +
+ Your average + {#if averageIntake} + {averageIntake} kcal + {:else} + No data yet + {/if} +
+ + +
+
+
+ {/if} + + + {#if averageIntake} +
+ +
+ +
+
+ {/if}
- {#if isHolding} -
-

- Your calorie target is set to maintain your current weight. Stay within this range to keep - your weight stable. -

+ + + {#if !isHolding || dailyRate !== 0} +
+ Planned {rateLabel} + {dailyRate} kcal +
+ {/if} + + {#if actualDeficit != null} +
+ Actual {rateLabel} + {Math.abs(actualDeficit)} kcal +
+ {/if} + + {#if showMaximum} +
+ Maximum + {Math.abs(maximumCalories)} kcal
{/if} + + {#if isHolding} +

+ Your target is set to maintain current weight. Stay within this range. +

+ {/if}
+ + diff --git a/src/lib/component/journey/CaloriePlanCard.test.ts b/src/lib/component/journey/CaloriePlanCard.test.ts index 5dc64b36..182976dc 100644 --- a/src/lib/component/journey/CaloriePlanCard.test.ts +++ b/src/lib/component/journey/CaloriePlanCard.test.ts @@ -1,7 +1,36 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { render, screen } from '@testing-library/svelte'; import CaloriePlanCard from './CaloriePlanCard.svelte'; +// Mock NumberFlow +vi.mock('@number-flow/svelte', () => { + const NumberFlowMock = function (anchor: any, props: any) { + const value = props?.value ?? 0; + const textNode = document.createTextNode(String(value)); + + if (anchor && anchor.parentNode) { + anchor.parentNode.insertBefore(textNode, anchor); + } + + return { + p: (newProps: any) => { + if (newProps.value !== undefined) { + textNode.textContent = String(newProps.value); + } + }, + d: () => { + if (textNode.parentNode) { + textNode.parentNode.removeChild(textNode); + } + } + }; + }; + + return { + default: NumberFlowMock + }; +}); + describe('CaloriePlanCard Component', () => { it('should render calorie plan title', () => { render(CaloriePlanCard, { @@ -16,8 +45,8 @@ describe('CaloriePlanCard Component', () => { expect(screen.getByText('Calorie Plan')).toBeInTheDocument(); }); - it('should display daily deficit for weight loss', () => { - render(CaloriePlanCard, { + it('should display hero target calories number', () => { + const { container } = render(CaloriePlanCard, { props: { recommendation: 'LOSE', dailyRate: 500, @@ -26,12 +55,26 @@ describe('CaloriePlanCard Component', () => { } }); - expect(screen.getByText('Daily Deficit')).toBeInTheDocument(); - expect(screen.getByText('500 kcal')).toBeInTheDocument(); + expect(container.textContent).toContain('1800'); + expect(container.textContent).toContain('kcal / day'); }); - it('should display daily surplus for weight gain', () => { - render(CaloriePlanCard, { + it('should display planned deficit for weight loss', () => { + const { container } = render(CaloriePlanCard, { + props: { + recommendation: 'LOSE', + dailyRate: 500, + targetCalories: 1800, + maximumCalories: 2000 + } + }); + + expect(container.textContent).toContain('Planned deficit'); + expect(container.textContent).toContain('500 kcal'); + }); + + it('should display planned surplus for weight gain', () => { + const { container } = render(CaloriePlanCard, { props: { recommendation: 'GAIN', dailyRate: 300, @@ -40,12 +83,12 @@ describe('CaloriePlanCard Component', () => { } }); - expect(screen.getByText('Daily Surplus')).toBeInTheDocument(); - expect(screen.getByText('300 kcal')).toBeInTheDocument(); + expect(container.textContent).toContain('Planned surplus'); + expect(container.textContent).toContain('300 kcal'); }); - it('should display target intake with correct label for weight loss', () => { - render(CaloriePlanCard, { + it('should display goal label for weight loss', () => { + const { container } = render(CaloriePlanCard, { props: { recommendation: 'LOSE', dailyRate: 500, @@ -54,12 +97,11 @@ describe('CaloriePlanCard Component', () => { } }); - expect(screen.getByText('Target Intake (Loss)')).toBeInTheDocument(); - expect(screen.getByText('1800 kcal')).toBeInTheDocument(); + expect(container.textContent).toContain('Lose Weight'); }); - it('should display target intake with correct label for weight gain', () => { - render(CaloriePlanCard, { + it('should display goal label for weight gain', () => { + const { container } = render(CaloriePlanCard, { props: { recommendation: 'GAIN', dailyRate: 300, @@ -68,12 +110,11 @@ describe('CaloriePlanCard Component', () => { } }); - expect(screen.getByText('Target Intake (Gain)')).toBeInTheDocument(); - expect(screen.getByText('2500 kcal')).toBeInTheDocument(); + expect(container.textContent).toContain('Gain Weight'); }); - it('should display maximum calories', () => { - render(CaloriePlanCard, { + it('should display target calories in bar label', () => { + const { container } = render(CaloriePlanCard, { props: { recommendation: 'LOSE', dailyRate: 500, @@ -82,12 +123,12 @@ describe('CaloriePlanCard Component', () => { } }); - expect(screen.getByText('Maximum Limit')).toBeInTheDocument(); - expect(screen.getByText('2000 kcal')).toBeInTheDocument(); + expect(container.textContent).toContain('Target'); + expect(container.textContent).toContain('1800 kcal'); }); it('should handle HOLD recommendation', () => { - render(CaloriePlanCard, { + const { container } = render(CaloriePlanCard, { props: { recommendation: 'HOLD', dailyRate: 0, @@ -96,12 +137,12 @@ describe('CaloriePlanCard Component', () => { } }); - expect(screen.getByText('Target Intake (Maintain)')).toBeInTheDocument(); - expect(screen.getByText('2200 kcal')).toBeInTheDocument(); + expect(container.textContent).toContain('Maintain Weight'); + expect(container.textContent).toContain('2200'); }); it('should show maintenance message for HOLD', () => { - render(CaloriePlanCard, { + const { container } = render(CaloriePlanCard, { props: { recommendation: 'HOLD', dailyRate: 0, @@ -110,9 +151,7 @@ describe('CaloriePlanCard Component', () => { } }); - expect( - screen.getByText(/Your calorie target is set to maintain your current weight/i) - ).toBeInTheDocument(); + expect(container.textContent).toMatch(/maintain current weight/i); }); it('should not show daily rate when HOLD with zero rate', () => { @@ -125,8 +164,67 @@ describe('CaloriePlanCard Component', () => { } }); - // Should not show "Daily Adjustment" when rate is 0 - expect(screen.queryByText('Daily Adjustment')).not.toBeInTheDocument(); + expect(container.textContent).not.toContain('Planned adjustment'); + }); + + it('should render target bar', () => { + const { container } = render(CaloriePlanCard, { + props: { + recommendation: 'LOSE', + dailyRate: 500, + targetCalories: 1800, + maximumCalories: 2000 + } + }); + + const bar = container.querySelector('.bg-primary.rounded-full'); + expect(bar).toBeTruthy(); + }); + + it('should render average intake bar when provided', () => { + const { container } = render(CaloriePlanCard, { + props: { + recommendation: 'LOSE', + dailyRate: 500, + targetCalories: 1800, + maximumCalories: 2000, + averageIntake: 1600 + } + }); + + expect(container.textContent).toContain('1600 kcal'); + expect(container.textContent).toContain('Your average'); + const bar = container.querySelector('.bg-accent.rounded-full'); + expect(bar).toBeTruthy(); + }); + + it('should show accent bar when average exceeds target', () => { + const { container } = render(CaloriePlanCard, { + props: { + recommendation: 'LOSE', + dailyRate: 500, + targetCalories: 1800, + maximumCalories: 2000, + averageIntake: 1900 + } + }); + + const bar = container.querySelector('.bg-accent.rounded-full'); + expect(bar).toBeTruthy(); + }); + + it('should show no data message when average intake is zero', () => { + const { container } = render(CaloriePlanCard, { + props: { + recommendation: 'LOSE', + dailyRate: 500, + targetCalories: 1800, + maximumCalories: 2000, + averageIntake: 0 + } + }); + + expect(container.textContent).toContain('No data yet'); }); it('should apply correct styling', () => { diff --git a/src/lib/component/journey/EncouragementMessage.svelte b/src/lib/component/journey/EncouragementMessage.svelte index f515507e..1700379a 100644 --- a/src/lib/component/journey/EncouragementMessage.svelte +++ b/src/lib/component/journey/EncouragementMessage.svelte @@ -1,8 +1,39 @@ - - Remember: -

Consistency is key. Small daily actions lead to big results!

+ +

{message.text}

diff --git a/src/lib/component/journey/EncouragementMessage.test.ts b/src/lib/component/journey/EncouragementMessage.test.ts index 0f02da55..3d5cf6a7 100644 --- a/src/lib/component/journey/EncouragementMessage.test.ts +++ b/src/lib/component/journey/EncouragementMessage.test.ts @@ -7,38 +7,65 @@ import { setupVeilchenMock } from '../../../../tests/utils/mocks'; setupVeilchenMock(); describe('EncouragementMessage Component', () => { - it('should render the encouragement message', () => { - render(EncouragementMessage); + const defaultProps = { + daysElapsed: 10, + daysLeft: 100, + averageIntake: 0, + targetCalories: 1800, + goalReached: false + }; - expect(screen.getByText('Remember:')).toBeInTheDocument(); + it('should render a contextual message', () => { + const { container } = render(EncouragementMessage, { props: defaultProps }); + + expect(container.querySelector('.text-sm')).toBeDefined(); + }); + + it('should show goal reached message', () => { + const { container } = render(EncouragementMessage, { + props: { ...defaultProps, goalReached: true } + }); + + expect(container.textContent).toContain('You did it!'); + }); + + it('should show near-end message when close to finish', () => { + const { container } = render(EncouragementMessage, { + props: { ...defaultProps, daysLeft: 10, daysElapsed: 100, averageIntake: 1500 } + }); + + expect(container.textContent).toContain('finish line'); }); - it('should display the motivational text', () => { - render(EncouragementMessage); + it('should show early start message for new journeys', () => { + const { container } = render(EncouragementMessage, { + props: { ...defaultProps, daysElapsed: 1, daysLeft: 120 } + }); - expect( - screen.getByText('Consistency is key. Small daily actions lead to big results!') - ).toBeInTheDocument(); + expect(container.textContent).toContain('Great start'); }); - it('should render as an info alert', () => { - const { container } = render(EncouragementMessage); + it('should show no-data message when average intake is zero', () => { + const { container } = render(EncouragementMessage, { + props: { ...defaultProps, averageIntake: 0 } + }); - // The component uses AlertBox with AlertType.Info - expect(container.querySelector('.alert')).toBeDefined(); + expect(container.textContent).toContain('Consistency'); }); - it('should apply correct text styling', () => { - const { container } = render(EncouragementMessage); + it('should show on-target message when average is within target', () => { + const { container } = render(EncouragementMessage, { + props: { ...defaultProps, averageIntake: 1700, targetCalories: 1800 } + }); - const textElement = container.querySelector('.text-sm'); - expect(textElement).toBeDefined(); + expect(container.textContent).toContain('within your daily target'); }); - it('should render without props', () => { - const { container } = render(EncouragementMessage); + it('should show above-target message when averaging over', () => { + const { container } = render(EncouragementMessage, { + props: { ...defaultProps, averageIntake: 2100, targetCalories: 1800 } + }); - expect(container).toBeDefined(); - expect(screen.getByText('Remember:')).toBeInTheDocument(); + expect(container.textContent).toContain('above target'); }); }); diff --git a/src/lib/component/journey/JourneyTimeline.svelte b/src/lib/component/journey/JourneyTimeline.svelte index a3c199a2..a7cee8f9 100644 --- a/src/lib/component/journey/JourneyTimeline.svelte +++ b/src/lib/component/journey/JourneyTimeline.svelte @@ -1,7 +1,7 @@ -
-
-

Journey Timeline

+
+
+

Journey Timeline

+ {daysLeft} days left +
-
    - -
  • -
    -
    -
    - - - {convertDateStrToDisplayDateStr(startDate)} - -
    -
    - kg -
    - Starting Weight -
    -
    -
    -
    -
    -
    -
  • + +
    +
    +
    - -
  • -
    -
    -
    -
    -
    -
    -
    - - Today -
    -
    - kg -
    -
    - {#if weightChange === 0} - No change yet - {:else if isOnTrack} - {#if isGaining} - - +{Math.abs(weightChange).toFixed(1)} kg gained - {:else} - - {Math.abs(weightChange).toFixed(1)} kg lost - {/if} - {/if} -
    -
    -
    -
    -
  • + +
    +
    + + kg + + {convertDateStrToDisplayDateStr(startDate)} +
    +
    + + kg + + {convertDateStrToDisplayDateStr(endDate)} +
    +
    - -
  • -
    -
    -
    -
    - - - {convertDateStrToDisplayDateStr(endDate)} - -
    -
    - kg -
    - Target Weight -
    -
    -
    -
    -
    -
  • -
+ +
+
+ + Today +
+
+ + kg + + {#if weightChange === 0} + No change yet + {:else if isOnTrack} + {#if isGaining} + + {:else} + + {/if} + {Math.abs(weightChange).toFixed(1)} kg + {/if} +
+ + diff --git a/src/lib/component/journey/JourneyTimeline.test.ts b/src/lib/component/journey/JourneyTimeline.test.ts index 1d10ce29..5064b44a 100644 --- a/src/lib/component/journey/JourneyTimeline.test.ts +++ b/src/lib/component/journey/JourneyTimeline.test.ts @@ -34,43 +34,22 @@ describe('JourneyTimeline Component', () => { expect(screen.getByText('Journey Timeline')).toBeInTheDocument(); }); - it('should display starting weight label', () => { - render(JourneyTimeline, { props: mockProps }); - - expect(screen.getByText('Starting Weight')).toBeInTheDocument(); - }); - it('should display today label', () => { render(JourneyTimeline, { props: mockProps }); expect(screen.getByText('Today')).toBeInTheDocument(); }); - it('should display target weight label', () => { - render(JourneyTimeline, { props: mockProps }); + it('should display days left', () => { + const { container } = render(JourneyTimeline, { props: mockProps }); - expect(screen.getByText('Target Weight')).toBeInTheDocument(); + expect(container.textContent).toMatch(/days left/); }); it('should show weight loss progress', () => { - render(JourneyTimeline, { props: mockProps }); - - // Should show "kg lost" for weight loss - expect(screen.getByText(/kg lost/i)).toBeInTheDocument(); - }); - - it('should show weight gain progress', () => { - render(JourneyTimeline, { - props: { - ...mockProps, - initialWeight: 60, - targetWeight: 70, - currentWeight: 65 - } - }); + const { container } = render(JourneyTimeline, { props: mockProps }); - // Should show "kg gained" for weight gain - expect(screen.getByText(/kg gained/i)).toBeInTheDocument(); + expect(container.textContent).toMatch(/5\.0 kg/); }); it('should show no change message when weight unchanged', () => { @@ -84,28 +63,25 @@ describe('JourneyTimeline Component', () => { expect(screen.getByText('No change yet')).toBeInTheDocument(); }); - it('should render timeline structure', () => { + it('should render progress bar', () => { const { container } = render(JourneyTimeline, { props: mockProps }); - const timeline = container.querySelector('.timeline.timeline-vertical'); - expect(timeline).toBeDefined(); - - // Should have 3 timeline items (start, current, target) - const timelineItems = container.querySelectorAll('.timeline'); - expect(timelineItems.length).toBeGreaterThan(0); + const track = container.querySelector('.journey-bar-track'); + const fill = container.querySelector('.journey-bar-fill'); + expect(track).toBeTruthy(); + expect(fill).toBeTruthy(); }); - it('should apply correct card styling', () => { + it('should apply primary card styling', () => { const { container } = render(JourneyTimeline, { props: mockProps }); - const wrapper = container.querySelector('.bg-base-100.rounded-box.p-6.shadow'); - expect(wrapper).toBeDefined(); + const wrapper = container.querySelector('.bg-primary'); + expect(wrapper).toBeTruthy(); }); it('should display formatted dates', () => { render(JourneyTimeline, { props: mockProps }); - // Check that dates are rendered (formatted by mock) expect(screen.getByText(/Jan.*1.*2025/i)).toBeInTheDocument(); expect(screen.getByText(/Jun.*1.*2025/i)).toBeInTheDocument(); }); @@ -121,45 +97,14 @@ describe('JourneyTimeline Component', () => { } }); - // Should show timeline even when maintaining weight expect(screen.getByText('Journey Timeline')).toBeInTheDocument(); expect(screen.getByText('Today')).toBeInTheDocument(); }); - it('should show calendar icon for start date', () => { + it('should render today callout with inverted colors', () => { const { container } = render(JourneyTimeline, { props: mockProps }); - // Check for timeline structure with calendar icon - expect(container.querySelector('.timeline-start')).toBeDefined(); - }); - - it('should show lightning icon for current date', () => { - const { container } = render(JourneyTimeline, { props: mockProps }); - - // Check for current date with accent styling - const currentBox = container.querySelector('.bg-secondary.text-secondary-content'); - expect(currentBox).toBeDefined(); - }); - - it('should show target icon for end date', () => { - const { container } = render(JourneyTimeline, { props: mockProps }); - - // Check for target date timeline item - expect(container.querySelector('.timeline-start')).toBeDefined(); - }); - - it('should calculate weight change correctly', () => { - render(JourneyTimeline, { - props: { - startDate: '2025-01-01', - endDate: '2025-06-01', - initialWeight: 80, - targetWeight: 70, - currentWeight: 75 - } - }); - - // 80 - 75 = 5 kg lost - expect(screen.getByText('5.0 kg lost')).toBeInTheDocument(); + const todayCallout = container.querySelector('.bg-primary-content.text-primary'); + expect(todayCallout).toBeTruthy(); }); }); diff --git a/src/lib/component/journey/UserProfileCard.svelte b/src/lib/component/journey/UserProfileCard.svelte index f12993d7..c43f1bfb 100644 --- a/src/lib/component/journey/UserProfileCard.svelte +++ b/src/lib/component/journey/UserProfileCard.svelte @@ -16,7 +16,7 @@ let ActivityIcon = $derived(activityInfo.icon); -
+
diff --git a/src/lib/component/navigation/HistoryIcon.svelte b/src/lib/component/navigation/HistoryIcon.svelte new file mode 100644 index 00000000..186adfb2 --- /dev/null +++ b/src/lib/component/navigation/HistoryIcon.svelte @@ -0,0 +1,13 @@ + + + + + + diff --git a/src/lib/component/navigation/JournalIcon.svelte b/src/lib/component/navigation/JournalIcon.svelte new file mode 100644 index 00000000..ae58b3de --- /dev/null +++ b/src/lib/component/navigation/JournalIcon.svelte @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/src/lib/component/navigation/ProgressIcon.svelte b/src/lib/component/navigation/ProgressIcon.svelte new file mode 100644 index 00000000..936abbd5 --- /dev/null +++ b/src/lib/component/navigation/ProgressIcon.svelte @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + diff --git a/src/lib/component/navigation/SettingsIcon.svelte b/src/lib/component/navigation/SettingsIcon.svelte new file mode 100644 index 00000000..9a575713 --- /dev/null +++ b/src/lib/component/navigation/SettingsIcon.svelte @@ -0,0 +1,42 @@ + + + + + + + + + + + + + diff --git a/src/lib/component/weight/WeightScore.svelte b/src/lib/component/weight/WeightScore.svelte index 34ab0524..404c1342 100644 --- a/src/lib/component/weight/WeightScore.svelte +++ b/src/lib/component/weight/WeightScore.svelte @@ -3,15 +3,15 @@ import type { WeightTarget, WeightTracker } from '$lib/api/gen'; import NumberFlow from '@number-flow/svelte'; import { differenceInDays } from 'date-fns'; - import { ShieldCheck, ShieldWarning, TrendDown, TrendUp } from 'phosphor-svelte'; - import { goto } from '$app/navigation'; + import { HandTap, ShieldCheck, ShieldWarning, TrendDown, TrendUp } from 'phosphor-svelte'; interface Props { weightTracker: WeightTracker; weightTarget: WeightTarget; + onupdate?: () => void; } - let { weightTracker, weightTarget }: Props = $props(); + let { weightTracker, weightTarget, onupdate }: Props = $props(); let percentage = $derived.by(() => { const diff = weightTarget.initialWeight - weightTracker.amount; @@ -24,9 +24,11 @@ let lastEntryDayDiff = $derived( differenceInDays(new Date(), parseStringAsDate(weightTracker?.added!)) ); + + let needsUpdate = $derived(lastEntryDayDiff > 0); -
+{#snippet weightContent()}
Current Weight
@@ -34,7 +36,10 @@ kg - {#if modifier === '-'} + {#if needsUpdate} + Tap to update + + {:else if modifier === '-'} {:else if modifier === '+'} @@ -54,27 +59,23 @@ Last update: {lastEntryDayDiff} days ago. {/if} - - {modifier}{Math.abs(percentage)}% - + {#if !needsUpdate} + {modifier}{Math.abs(percentage)}% + {/if}
-
-{#if weightTarget} - {@const dayDiff = differenceInDays(parseStringAsDate(weightTarget.endDate), new Date())} - {@const totalDays = differenceInDays( - parseStringAsDate(weightTarget.endDate), - parseStringAsDate(weightTarget.startDate) - )} - {@const progress = totalDays === 0 ? 0 : Math.round(((totalDays - dayDiff) / totalDays) * 100)} -
- -

- {dayDiff} days left. -

- -
+{/snippet} - +{#if needsUpdate} + +{:else} +
+ {@render weightContent()}
{/if} @@ -83,11 +84,6 @@ font-size: 2rem; } - .progress-container { - padding-inline: calc(0.25rem * 6); - padding-block: calc(0.25rem * 4); - } - .weight-stat { border-right: 0px !important; } diff --git a/src/lib/component/weight/WeightScore.test.ts b/src/lib/component/weight/WeightScore.test.ts index f6e83f09..1c07b07e 100644 --- a/src/lib/component/weight/WeightScore.test.ts +++ b/src/lib/component/weight/WeightScore.test.ts @@ -75,8 +75,8 @@ describe('WeightScore', () => { expect(container.textContent).toMatch(/82 kg/i); }); - it('should show dash when no weight data', () => { - const newEntry: WeightTracker = { + it('should show tap to update when entry is stale', () => { + const staleEntry: WeightTracker = { id: 1, added: '2024-01-20', time: '09:15:00', @@ -85,12 +85,12 @@ describe('WeightScore', () => { const { container } = render(WeightScore, { props: { - weightTracker: newEntry, + weightTracker: staleEntry, weightTarget: mockWeightTarget } }); - expect(container.textContent).toContain('-'); + expect(container.textContent).toContain('Tap to update'); }); }); @@ -201,47 +201,7 @@ describe('WeightScore', () => { }); }); - describe('Progress Information', () => { - it('should show days left in target period', () => { - // Mock with a target that ends in the future - const futureTarget: WeightTarget = { - ...mockWeightTarget, - endDate: '2099-12-31' // Far future - }; - - const { container } = render(WeightScore, { - props: { - weightTracker: mockWeightTracker, - weightTarget: futureTarget - } - }); - - expect(container.textContent).toMatch(/\d+ days left/i); - }); - - it('should show review plan button', () => { - render(WeightScore, { - props: { - weightTracker: mockWeightTracker, - weightTarget: mockWeightTarget - } - }); - - expect(screen.getByText(/Review plan/i)).toBeTruthy(); - }); - - it('should show progress bar', () => { - const { container } = render(WeightScore, { - props: { - weightTracker: mockWeightTracker, - weightTarget: mockWeightTarget - } - }); - - const progressBar = container.querySelector('progress.progress'); - expect(progressBar).toBeTruthy(); - }); - }); + // Progress bar, "days left", and "Review plan" have been moved to the dashboard header describe('Edge Cases', () => { it('should handle very large weight values', () => { @@ -318,15 +278,6 @@ describe('WeightScore', () => { expect(container.querySelector('.stat-desc')).toBeTruthy(); }); - it('should have progress container', () => { - const { container } = render(WeightScore, { - props: { - weightTracker: mockWeightTracker, - weightTarget: mockWeightTarget - } - }); - - expect(container.querySelector('.progress-container')).toBeTruthy(); - }); + // Progress container has been moved to the dashboard header }); }); diff --git a/src/lib/component/wizard/Finish.svelte b/src/lib/component/wizard/Finish.svelte index 87d4a9c5..99f211ff 100644 --- a/src/lib/component/wizard/Finish.svelte +++ b/src/lib/component/wizard/Finish.svelte @@ -1,10 +1,10 @@ -
+

Dashboard

-
- {#if data.dashboardData.currentDay > 0} - Day {data.dashboardData.currentDay} - {:else} - - {/if} -
-
- + +
+
+
+ {#if data.dashboardData.currentDay > 0} + Day {data.dashboardData.currentDay} + {/if} + {displayDate} +
- -
+ {#if avatarSrc} + + {/if} +
-
- -
+ +
+
+ {dayDiff} days left + +
+
+
+
- -
-
- -
- + + {#if showPlan} +
+
+
+ + kg + + {convertDateStrToDisplayDateStr(weightTarget.startDate)} +
+
+ + kg + + {convertDateStrToDisplayDateStr(weightTarget.endDate)} +
+
+ + +
+
+ + {weightLabel} +
+
+ + kg + + {#if weightChange === 0} + No change yet + {:else if isOnTrack} + {#if isGaining} + + {:else} + + {/if} + {Math.abs(weightChange).toFixed(1)} kg + {/if} +
+
+
+ {/if} +
+ + + {#if showPlan} +
+
+ weightTarget.initialWeight + ? 'GAIN' + : 'LOSE'} + targetCalories={intakeTarget.targetCalories} + maximumCalories={intakeTarget.maximumCalories} + {averageIntake} + /> +
+ +
+ +
+
+ {/if}
- - + +
+
+ + +
- - - +
+ +
+
+ {#snippet title()} - Add Intake + Add Intake {#if modal.currentEntry} - - Date: {convertDateStrToDisplayDateStr((modal.currentEntry as NewIntake).added)} + + {convertDateStrToDisplayDateStr((modal.currentEntry as NewIntake).added)} {/if} {/snippet} @@ -163,18 +336,16 @@ {#snippet title()} - Edit Intake - + Edit Intake + {#if modal.currentEntry} - - Added: {convertDateStrToDisplayDateStr((modal.currentEntry as Intake).added)} + + {convertDateStrToDisplayDateStr((modal.currentEntry as Intake).added)} {/if} - - - + {/snippet} @@ -201,9 +372,9 @@ oncancel={modalWeight.cancel} > {#snippet title()} - Set Weight - - Date: {getDateAsStr(new Date(), display_date_format)} + Set Weight + + {getDateAsStr(new Date(), display_date_format)} {/snippet} {#snippet content()} @@ -230,3 +401,24 @@ {/snippet} + + diff --git a/src/routes/(app)/about/+page.svelte b/src/routes/(app)/about/+page.svelte index 932754dd..c43cc348 100644 --- a/src/routes/(app)/about/+page.svelte +++ b/src/routes/(app)/about/+page.svelte @@ -1,11 +1,12 @@ -
+

History

-
+ +
{#if selectedDateStr} {@const selectedDate = parseStringAsDate(selectedDateStr)} -
- - {getDateAsStr(selectedDate, 'MMMM yyyy')} - +
+ {getDateAsStr(selectedDate, 'MMMM yyyy')} +
+ + + + avg kcal/day +
{/if} + +
({ timeframe: 300, minSwipeDistance: 60, touchAction: 'pan-y' })} onswipe={handleWeekSwipe} > - {#each dates as dateStr} @@ -243,20 +301,18 @@ {@const dayName = getDateAsStr(parseStringAsDate(dateStr), 'EE')} {/each} {#if showRightCaret} - {:else}
@@ -264,20 +320,19 @@
-
+ +
{#key selectedDateStr} -
+
+
({ timeframe: 300, minSwipeDistance: 60, touchAction: 'pan-y' })} onswipe={handleDaySwipe} > -
-
-
Average calories
-
-
-
- c.amount)} @@ -285,66 +340,110 @@ />
-
- {#each intakeHistory as calories} - edit(calories)} onright={() => remove(calories)}> - {#snippet leftAction()} - - {/snippet} + +
+ {#each foodCategories as cat (cat.shortvalue)} + {@const Icon = getFoodCategoryIcon(cat.shortvalue)} + {@const isTracked = intakeHistory.some((e) => e.category === cat.shortvalue)} - {#snippet rightAction()} - - {/snippet} + + {/each} +
-
edit(calories)} - > -
- - {calories.description} - - - {calories.amount} kcal - -
- {getFoodCategoryLongvalue(foodCategories, calories.category)} + {#if intakeHistory.length > 0} +
+ {#each intakeHistory as calories, i} + edit(calories)} onright={() => remove(calories)}> + {#snippet leftAction()} + + {/snippet} + + {#snippet rightAction()} + + {/snippet} + +
edit(calories)} > -
- {/each} +
+ + {calories.description} + + + {calories.amount} kcal + +
+ {getFoodCategoryLongvalue(foodCategories, calories.category)} +
+ + {/each} +
+ {:else} +
+ +
+

No meals logged

+

Tap below to start tracking

+
+
+ {/if} - -
-
({ timeframe: 300, minSwipeDistance: 60, touchAction: 'pan-y' })} - onswipe={handleDaySwipe} - > -
-
Weight
+ - {#if weightHistory.length > 0} -
- {weightHistory[0].amount} kg + +
+ {#if weightHistory.length > 0} + editWeight(weightHistory[0])}> + {#snippet leftAction()} + + {/snippet} +
+ Weight +
+ + kg +
- {:else} -
No weight tracked.
- {/if} -
+ + {:else} +
+
+ Weight + +
No weight tracked.
+
+ + +
+ {/if}
{/key}
+ {#snippet title()} - Add Intake + Add Intake Date: {convertDateStrToDisplayDateStr(selectedDateStr)} @@ -357,10 +456,11 @@ {/snippet} + {#snippet title()} {#if modal.currentEntry} - Edit Intake + Edit Intake Added: {convertDateStrToDisplayDateStr((modal.currentEntry as Intake).added)} @@ -390,6 +490,7 @@ {/snippet} + Cancel {/snippet} + + + + {#snippet title()} + Set Weight + {#if modalWeight.currentEntry} + + {convertDateStrToDisplayDateStr((modalWeight.currentEntry as WeightTracker).added)} + + {/if} + {/snippet} + {#snippet content()} +
+ {#if modalWeight.errorMessage} +
+ {modalWeight.errorMessage} +
+ {/if} + {#if modalWeight.currentEntry} + + {/if} +
+ {/snippet} +
+ + + + {#snippet title()} + Set Weight + {#if modalWeight.currentEntry} + + {convertDateStrToDisplayDateStr((modalWeight.currentEntry as WeightTracker).added)} + + {/if} + {/snippet} + {#snippet content()} +
+ {#if modalWeight.errorMessage} +
+ {modalWeight.errorMessage} +
+ {/if} + {#if modalWeight.currentEntry} + + {/if} +
+ {/snippet} +
diff --git a/src/routes/(app)/import/+page.svelte b/src/routes/(app)/import/+page.svelte index 29355c43..b630011d 100644 --- a/src/routes/(app)/import/+page.svelte +++ b/src/routes/(app)/import/+page.svelte @@ -7,6 +7,7 @@ ImportTableSchema, type ImportResult } from '$lib/api/gen/types'; + import SettingsIcon from '$lib/component/navigation/SettingsIcon.svelte'; import { Channel } from '@tauri-apps/api/core'; import { open } from '@tauri-apps/plugin-dialog'; import { debug, error } from '@tauri-apps/plugin-log'; @@ -22,7 +23,7 @@ type BreadcrumbItem, type OptionCardData } from '@thwbh/veilchen'; - import { Check, Gear, Hamburger, Scales, Upload, Warning } from 'phosphor-svelte'; + import { Check, ForkKnife, Scales, Upload, Warning } from 'phosphor-svelte'; const ImportFormat = ImportFormatSchema.enum; const ImportTable = ImportTableSchema.enum; @@ -56,8 +57,7 @@ const items: BreadcrumbItem[] = [ { id: '1', - icon: Gear, - iconProps: { weight: 'bold' } + icon: SettingsIcon }, { id: '2', @@ -185,7 +185,7 @@ {#snippet icon(option)} {#if option.value === ImportTable.intake} - + {:else if option.value === ImportTable.weightTracker} {/if} diff --git a/src/routes/(app)/profile/+page.svelte b/src/routes/(app)/profile/+page.svelte index ef60fcc9..c648e8a6 100644 --- a/src/routes/(app)/profile/+page.svelte +++ b/src/routes/(app)/profile/+page.svelte @@ -9,7 +9,7 @@ } from '@thwbh/veilchen'; import type { BreadcrumbItem } from '@thwbh/veilchen'; import { getUserContext } from '$lib/context'; - import { Gear, IdentificationCard, PencilSimple } from 'phosphor-svelte'; + import { IdentificationCard, PencilSimple } from 'phosphor-svelte'; import type { LibreUser } from '$lib/api/index.js'; import { updateUser } from '$lib/api/gen/commands'; import UserAvatar from '$lib/component/profile/UserAvatar.svelte'; @@ -18,6 +18,7 @@ import { getAvatar } from '$lib/avatar'; import { slide } from 'svelte/transition'; import { useEntryModal } from '$lib/composition/useEntryModal.svelte'; + import SettingsIcon from '$lib/component/navigation/SettingsIcon.svelte'; let { data } = $props(); @@ -64,8 +65,7 @@ const items: BreadcrumbItem[] = [ { id: '1', - icon: Gear, - iconProps: { weight: 'bold' } + icon: SettingsIcon }, { id: '2', diff --git a/src/routes/(app)/progress/+page.svelte b/src/routes/(app)/progress/+page.svelte index 7ff8e028..5bc03b89 100644 --- a/src/routes/(app)/progress/+page.svelte +++ b/src/routes/(app)/progress/+page.svelte @@ -1,154 +1,307 @@ -
+

Progress

- Your progress - + +
+
+ Your Progress + Day {daysPassed} of {daysTotal} +
-
-
-
Starting weight
-
- {data.trackerProgress.weightChartData.values[0]}kg + +
+
+
-
-
Current weight
-
- {data.trackerProgress.weightChartData.values[ - data.trackerProgress.weightChartData.values.length - 1 - ]} - kg + + +
+
+ + kg + + Start +
+ +
+ {#if weightDiff === 0} + No change + {:else} + {#if weightDiff > 0} + + {:else} + + {/if} + {Math.abs(weightDiff).toFixed(1)} kg + {/if} +
+ +
+ + kg + + Current
- - -
-
-
Average per day
-
- {data.trackerProgress.intakeChartData.avg} - kcal + +
+ +
+
+

Weight

+
+ + Actual + + + Target + +
-
Target: {data.trackerProgress.intakeTarget.targetCalories} kcal
+
-
-
∅ Deficit
-
- {data.trackerProgress.intakeTarget.maximumCalories - - data.trackerProgress.intakeChartData.dailyAverage} - kcal + + +
+
+

Calorie Intake

+
+ + Actual + + + + Target + +
-
- Target: {data.trackerProgress.intakeTarget.maximumCalories - - data.trackerProgress.intakeTarget.targetCalories} + + +
+
+ Average per day +
+ + kcal +
+ Target: {intakeTarget.targetCalories} kcal +
+
+ Average {rateLabel} +
+ + kcal +
+ Target: {targetDeficit} +
+ + + {#if categories.length > 0} +
+

By Category

+
+ {#each categories as [code, avg] (code)} + {@const Icon = getFoodCategoryIcon(code)} + {@const percent = Math.round((avg / maxCategoryAvg) * 100)} +
+
+ {#if Icon} + + {/if} +
+
+
+ {getFoodCategoryLongvalue(foodCategories, code) ?? code} + {Math.round(avg)} kcal +
+
+
+
+
+
+ {/each} +
+
+ {/if}
+ + diff --git a/src/routes/(app)/review/+page.svelte b/src/routes/(app)/review/+page.svelte deleted file mode 100644 index 7a8826e7..00000000 --- a/src/routes/(app)/review/+page.svelte +++ /dev/null @@ -1,97 +0,0 @@ - - -
-
-

Your Plan

- - -

- Track your progress and stay motivated on your fitness journey -

-
- - {#if !weightTarget} -
- No active weight plan found. Complete the setup wizard to create one. -
- {:else if mounted} -
- -
- -
- -
- -
- weightTarget.initialWeight ? 'GAIN' : 'LOSE'} - targetCalories={intakeTarget.targetCalories} - maximumCalories={intakeTarget.maximumCalories} - /> -
- -
- -
- {/if} -
diff --git a/src/routes/(app)/review/+page.ts b/src/routes/(app)/review/+page.ts deleted file mode 100644 index 54432dd9..00000000 --- a/src/routes/(app)/review/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - getBodyData, - getLastWeightTarget, - getLastWeightTracker, - getLastIntakeTarget -} from '$lib/api/gen/commands'; - -export const ssr = false; - -export async function load() { - return { - weightTarget: await getLastWeightTarget(), - lastWeightTracker: await getLastWeightTracker(), - intakeTarget: await getLastIntakeTarget(), - bodyData: await getBodyData() - }; -} diff --git a/src/routes/(app)/wizard/+page.svelte b/src/routes/(app)/wizard/+page.svelte index 8ccdf1d7..6ea1480e 100644 --- a/src/routes/(app)/wizard/+page.svelte +++ b/src/routes/(app)/wizard/+page.svelte @@ -1,18 +1,17 @@