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