diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 11c439b3..fdb9d0f6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -611,6 +611,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf 0.11.3", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf 0.11.3", + "phf_codegen 0.11.3", +] + [[package]] name = "cipher" version = "0.4.4" @@ -981,6 +1003,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + [[package]] name = "diesel" version = "2.2.6" @@ -1760,6 +1788,17 @@ dependencies = [ "walkdir", ] +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.8.0", + "ignore", + "walkdir", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -1917,6 +1956,15 @@ version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "hyper" version = "1.5.2" @@ -2385,6 +2433,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libmath" version = "0.2.1" @@ -3054,6 +3108,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "pbkdf2" version = "0.12.2" @@ -3070,6 +3133,49 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pest_meta" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "phf" version = "0.8.0" @@ -3753,7 +3859,7 @@ version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "039f57d22229db401af3458ca939300178e99e88b938573cea12b7c2b0f09724" dependencies = [ - "globwalk", + "globwalk 0.8.1", "once_cell", "regex", "rust-i18n-macro", @@ -3786,7 +3892,7 @@ checksum = "75d2844d36f62b5d6b66f9cf8f8cbdbbbdcdb5fd37a473a9cc2fb45fdcf485d2" dependencies = [ "arc-swap", "base62", - "globwalk", + "globwalk 0.8.1", "itertools", "lazy_static", "normpath", @@ -3970,6 +4076,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-rename-rule" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "794e44574226fc701e3be5c651feb7939038fc67fb73f6f4dd5c4ba90fd3be70" + [[package]] name = "serde-untagged" version = "0.1.6" @@ -4214,6 +4326,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -4714,9 +4836,9 @@ dependencies = [ [[package]] name = "tauri-typegen" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aadf5f4c4c3dd207bff41052e50c1265b79775a83632fe9520f060211eecd3fe" +checksum = "c2575b042cc6b58b14f87c0e5b2d4a05a45565f47b1c4e5cee60979e99efd20f" dependencies = [ "chrono", "clap", @@ -4725,8 +4847,10 @@ dependencies = [ "quote", "regex", "serde", + "serde-rename-rule", "serde_json", "syn 2.0.106", + "tera", "thiserror 2.0.11", "walkdir", ] @@ -4805,6 +4929,28 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tera" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk 0.9.1", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "slug", + "unicode-segmentation", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -5151,6 +5297,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uds_windows" version = "1.1.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 36b5050c..ca329de6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,7 +16,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "2.0.5", features = [] } -tauri-typegen = "0.3.1" +tauri-typegen = "0.4.0" [dependencies] serde_json = "1.0.137" diff --git a/src/lib/component/intake/IntakeMask.test.ts b/src/lib/component/intake/IntakeMask.test.ts index 0b2f6a7a..0da7458a 100644 --- a/src/lib/component/intake/IntakeMask.test.ts +++ b/src/lib/component/intake/IntakeMask.test.ts @@ -27,6 +27,7 @@ describe('IntakeMask', () => { const mockEntry: Intake = { id: 1, added: '2024-01-01', + time: '12:30:00', category: 'l', amount: 500, description: 'Healthy lunch' diff --git a/src/lib/component/intake/IntakeStack.test.ts b/src/lib/component/intake/IntakeStack.test.ts index 74a02cc9..959dfc4a 100644 --- a/src/lib/component/intake/IntakeStack.test.ts +++ b/src/lib/component/intake/IntakeStack.test.ts @@ -38,21 +38,24 @@ describe('IntakeStack', () => { added: '2024-01-01', category: 'b', amount: 400, - description: 'Oatmeal' + description: 'Oatmeal', + time: '08:30:00' }, { id: 2, added: '2024-01-01', category: 'l', amount: 600, - description: 'Salad' + description: 'Salad', + time: '13:10:00' }, { id: 3, added: '2024-01-01', category: 'd', amount: 800, - description: 'Pasta' + description: 'Pasta', + time: '19:15:00' } ]; diff --git a/src/lib/component/weight/WeightScore.svelte b/src/lib/component/weight/WeightScore.svelte index 21a37775..34ab0524 100644 --- a/src/lib/component/weight/WeightScore.svelte +++ b/src/lib/component/weight/WeightScore.svelte @@ -1,27 +1,17 @@
Current Weight
- {#if (weightTracker && 'id' in weightTracker) || lastWeightTracker} - {#if weightTracker && 'id' in weightTracker} - - {:else if lastWeightTracker} - - {/if} - {:else} - - - {/if} + kg @@ -57,24 +43,15 @@
- {#if weightTracker && 'id' in weightTracker} + {#if lastEntryDayDiff === 0} Last update: Today. - {:else if lastWeightTracker} - {@const lastEntryDayDiff = differenceInDays( - new Date(), - parseStringAsDate(lastWeightTracker.added) - )} - {#if lastEntryDayDiff > 2} - - Last update was {lastEntryDayDiff} days ago! - {:else} - - Last update: {lastEntryDayDiff} days ago. - {/if} + {:else if lastEntryDayDiff > 2} + + Last update was {lastEntryDayDiff} days ago! {:else} - - Nothing tracked yet. + + Last update: {lastEntryDayDiff} days ago. {/if} @@ -88,7 +65,7 @@ parseStringAsDate(weightTarget.endDate), parseStringAsDate(weightTarget.startDate) )} - {@const progress = Math.round(((totalDays - dayDiff) / totalDays) * 100)} + {@const progress = totalDays === 0 ? 0 : Math.round(((totalDays - dayDiff) / totalDays) * 100)}

diff --git a/src/lib/component/weight/WeightScore.test.ts b/src/lib/component/weight/WeightScore.test.ts index 1c2110ac..f6e83f09 100644 --- a/src/lib/component/weight/WeightScore.test.ts +++ b/src/lib/component/weight/WeightScore.test.ts @@ -1,8 +1,42 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, waitFor } from '@testing-library/svelte'; -import userEvent from '@testing-library/user-event'; +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; import type { WeightTarget, WeightTracker, NewWeightTracker } from '$lib/api/gen'; import WeightScore from './WeightScore.svelte'; +import { getDateAsStr } from '$lib/date'; +import { subDays } from 'date-fns'; + +// Mock NumberFlow as a simple component that renders the number +// For Svelte 5, components need $$render method for SSR and proper client-side mounting +vi.mock('@number-flow/svelte', () => { + const NumberFlowMock = function (anchor: any, props: any) { + const value = props?.value ?? 0; + const textNode = document.createTextNode(String(value)); + + // Insert the text node + if (anchor && anchor.parentNode) { + anchor.parentNode.insertBefore(textNode, anchor); + } + + return { + p: (newProps: any) => { + // update + if (newProps.value !== undefined) { + textNode.textContent = String(newProps.value); + } + }, + d: () => { + // destroy + if (textNode.parentNode) { + textNode.parentNode.removeChild(textNode); + } + } + }; + }; + + return { + default: NumberFlowMock + }; +}); describe('WeightScore', () => { const mockWeightTarget: WeightTarget = { @@ -17,12 +51,14 @@ describe('WeightScore', () => { const mockWeightTracker: WeightTracker = { id: 1, added: '2024-01-15', + time: '08:30:00', amount: 82 }; - const mockLastWeightTracker: WeightTracker = { + const mockWeightTrackerToday: WeightTracker = { id: 1, - added: '2024-01-10', + added: getDateAsStr(new Date()), + time: '12:00:00', amount: 83 }; @@ -36,32 +72,14 @@ describe('WeightScore', () => { }); // Check for kg unit and Last update message - expect(container.textContent).toContain('kg'); - expect(container.textContent).toContain('Last update: Today'); - }); - - it('should display last weight when only lastWeightTracker provided', () => { - const newEntry: NewWeightTracker = { - added: '2024-01-20', - amount: 81 - }; - - const { container } = render(WeightScore, { - props: { - weightTracker: newEntry, - lastWeightTracker: mockLastWeightTracker, - weightTarget: mockWeightTarget - } - }); - - // Check for kg unit and last update message - expect(container.textContent).toContain('kg'); - expect(container.textContent).toMatch(/Last update/i); + expect(container.textContent).toMatch(/82 kg/i); }); it('should show dash when no weight data', () => { - const newEntry: NewWeightTracker = { + const newEntry: WeightTracker = { + id: 1, added: '2024-01-20', + time: '09:15:00', amount: 81 }; @@ -77,26 +95,10 @@ describe('WeightScore', () => { }); describe('Status Messages', () => { - it('should show "Nothing tracked yet" when no data', () => { - const newEntry: NewWeightTracker = { - added: '2024-01-20', - amount: 81 - }; - - render(WeightScore, { - props: { - weightTracker: newEntry, - weightTarget: mockWeightTarget - } - }); - - expect(screen.getByText(/Nothing tracked yet/i)).toBeTruthy(); - }); - it('should show "Last update: Today" for current day entry', () => { render(WeightScore, { props: { - weightTracker: mockWeightTracker, + weightTracker: mockWeightTrackerToday, weightTarget: mockWeightTarget } }); @@ -106,20 +108,16 @@ describe('WeightScore', () => { it('should show days ago for old entries (warning < 2 days)', () => { // Create a tracker from 1 day ago - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - const yesterdayStr = yesterday.toISOString().split('T')[0]; - const oldTracker: WeightTracker = { id: 1, - added: yesterdayStr, + added: getDateAsStr(subDays(new Date(), 1)), + time: '08:30:00', amount: 83 }; const { container } = render(WeightScore, { props: { - weightTracker: { added: '2024-01-20', amount: 81 } as NewWeightTracker, - lastWeightTracker: oldTracker, + weightTracker: oldTracker, weightTarget: mockWeightTarget } }); @@ -130,20 +128,16 @@ describe('WeightScore', () => { it('should show critical warning for very old entries (> 2 days)', () => { // Create a tracker from 5 days ago - const fiveDaysAgo = new Date(); - fiveDaysAgo.setDate(fiveDaysAgo.getDate() - 5); - const oldDate = fiveDaysAgo.toISOString().split('T')[0]; - const veryOldTracker: WeightTracker = { id: 1, - added: oldDate, + added: getDateAsStr(subDays(new Date(), 5)), + time: '08:30:00', amount: 83 }; const { container } = render(WeightScore, { props: { - weightTracker: { added: '2024-01-20', amount: 81 } as NewWeightTracker, - lastWeightTracker: veryOldTracker, + weightTracker: veryOldTracker, weightTarget: mockWeightTarget } }); @@ -157,7 +151,7 @@ describe('WeightScore', () => { it('should show success state with ShieldCheck for current day', () => { render(WeightScore, { props: { - weightTracker: mockWeightTracker, + weightTracker: mockWeightTrackerToday, weightTarget: mockWeightTarget } }); @@ -168,20 +162,16 @@ describe('WeightScore', () => { it('should show warning state for entries 1-2 days old', () => { // Create a tracker from 1 day ago - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - const yesterdayStr = yesterday.toISOString().split('T')[0]; - const oldTracker: WeightTracker = { id: 1, - added: yesterdayStr, + added: getDateAsStr(subDays(new Date(), 1)), + time: '08:30:00', amount: 83 }; const { container } = render(WeightScore, { props: { - weightTracker: { added: '2024-01-20', amount: 81 } as NewWeightTracker, - lastWeightTracker: oldTracker, + weightTracker: oldTracker, weightTarget: mockWeightTarget } }); @@ -192,20 +182,16 @@ describe('WeightScore', () => { it('should show error state for entries older than 2 days', () => { // Create a tracker from 5 days ago - const fiveDaysAgo = new Date(); - fiveDaysAgo.setDate(fiveDaysAgo.getDate() - 5); - const oldDate = fiveDaysAgo.toISOString().split('T')[0]; - const veryOldTracker: WeightTracker = { id: 1, - added: oldDate, + added: getDateAsStr(subDays(new Date(), 5)), + time: '08:30:00', amount: 83 }; const { container } = render(WeightScore, { props: { - weightTracker: { added: '2024-01-20', amount: 81 } as NewWeightTracker, - lastWeightTracker: veryOldTracker, + weightTracker: veryOldTracker, weightTarget: mockWeightTarget } }); @@ -213,18 +199,6 @@ describe('WeightScore', () => { // ShieldWarning (error color) should show with "days ago!" (with exclamation) expect(container.textContent).toMatch(/Last update was.*5.*days ago!/i); }); - - it('should show neutral state with Shield icon when no data tracked', () => { - render(WeightScore, { - props: { - weightTracker: { added: '2024-01-20', amount: 81 } as NewWeightTracker, - weightTarget: mockWeightTarget - } - }); - - // Shield icon shows with "Nothing tracked yet" text - expect(screen.getByText(/Nothing tracked yet/i)).toBeTruthy(); - }); }); describe('Progress Information', () => { @@ -269,64 +243,12 @@ describe('WeightScore', () => { }); }); - describe('Callbacks', () => { - it('should call onAdd when provided', async () => { - const onaddMock = vi.fn().mockResolvedValue({ - id: 2, - added: '2024-01-20', - amount: 80 - }); - - render(WeightScore, { - props: { - weightTracker: mockWeightTracker, - weightTarget: mockWeightTarget, - onAdd: onaddMock - } - }); - - // Component should render without calling onadd immediately - expect(onaddMock).not.toHaveBeenCalled(); - }); - - it('should work without onAdd callback', () => { - expect(() => { - render(WeightScore, { - props: { - weightTracker: mockWeightTracker, - weightTarget: mockWeightTarget - } - }); - }).not.toThrow(); - }); - - it('should work without onEdit callback', () => { - expect(() => { - render(WeightScore, { - props: { - weightTracker: mockWeightTracker, - weightTarget: mockWeightTarget - } - }); - }).not.toThrow(); - }); - }); - describe('Edge Cases', () => { - it('should handle missing weightTracker', () => { - const { container } = render(WeightScore, { - props: { - weightTarget: mockWeightTarget - } - }); - - expect(container).toBeTruthy(); - }); - it('should handle very large weight values', () => { const heavyTracker: WeightTracker = { id: 1, - added: '2024-01-15', + added: getDateAsStr(new Date()), + time: '08:30:00', amount: 300 }; @@ -338,14 +260,15 @@ describe('WeightScore', () => { }); // Component should render without error - expect(container.textContent).toContain('kg'); + expect(container.textContent).toMatch(/300 kg/i); expect(container.textContent).toContain('Last update: Today'); }); it('should handle minimum weight values', () => { const lightTracker: WeightTracker = { id: 1, - added: '2024-01-15', + added: getDateAsStr(new Date()), + time: '08:30:00', amount: 30 }; @@ -356,14 +279,15 @@ describe('WeightScore', () => { } }); - expect(container.textContent).toContain('kg'); + expect(container.textContent).toMatch(/30 kg/i); expect(container.textContent).toContain('Last update: Today'); }); it('should handle fractional weight values', () => { const fractionalTracker: WeightTracker = { id: 1, - added: '2024-01-15', + added: getDateAsStr(new Date()), + time: '08:30:00', amount: 82.5 }; @@ -374,7 +298,7 @@ describe('WeightScore', () => { } }); - expect(container.textContent).toContain('kg'); + expect(container.textContent).toMatch(/82.5 kg/i); expect(container.textContent).toContain('Last update: Today'); }); }); diff --git a/src/lib/component/wizard/body/Report.svelte b/src/lib/component/wizard/body/Report.svelte index 95a80549..b063dd94 100644 --- a/src/lib/component/wizard/body/Report.svelte +++ b/src/lib/component/wizard/body/Report.svelte @@ -2,7 +2,6 @@ import { BmiCategorySchema, WizardRecommendationSchema, - type BmiCategory, type WizardInput, type WizardResult } from '$lib/api/gen'; @@ -36,7 +35,7 @@ BmiCategory.Overweight ]); - const getClassificationStyle = (category: BmiCategory) => { + const getClassificationStyle = (category: string) => { if (classificationLose.safeParse(category).success) { return 'badge-error'; } else if (category === BmiCategory.Underweight) { diff --git a/src/lib/enum.ts b/src/lib/enum.ts index 5ca6126d..8f61657c 100644 --- a/src/lib/enum.ts +++ b/src/lib/enum.ts @@ -1,20 +1,18 @@ -import type { BmiCategory } from '$lib/api/gen'; - export enum WizardOptions { - Default = 'DEFAULT', - Recommended = 'RECOMMENDED', - Custom_weight = 'CUSTOM_WEIGHT', - Custom_date = 'CUSTOM_DATE', - Custom = 'CUSTOM' + Default = 'DEFAULT', + Recommended = 'RECOMMENDED', + Custom_weight = 'CUSTOM_WEIGHT', + Custom_date = 'CUSTOM_DATE', + Custom = 'CUSTOM' } export function enumKeys(obj: object) { - return Object.keys(obj).filter((k) => Number.isNaN(+k)); + return Object.keys(obj).filter((k) => Number.isNaN(+k)); } -export const getBmiCategoryDisplayValue = (bmiCategory: BmiCategory) => { - return bmiCategory - .split(/(?=[A-Z])/) - .join(' ') - .toLowerCase(); +export const getBmiCategoryDisplayValue = (bmiCategory: string) => { + return bmiCategory + .split(/(?=[A-Z])/) + .join(' ') + .toLowerCase(); }; diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index fd25c962..4b191b80 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -32,7 +32,7 @@ let index: number = $state(0); let intake: Array = $state(dashboard.intakeTodayList); - let lastWeightTracker = $state(dashboard.weightMonthList[0]); + let lastWeightTracker: WeightTracker = $state(dashboard.weightMonthList[0]); const weightTarget: WeightTarget = dashboard.weightTarget; const intakeTarget: IntakeTarget = dashboard.intakeTarget; @@ -118,14 +118,7 @@

- createWeightTrackerEntry({ newEntry: entry })} - onEdit={(id, entry: WeightTracker) => - updateWeightTrackerEntry({ trackerId: id, updatedEntry: entry })} - /> +