@jjlmoya/utils-aquarium 1.56.0 → 1.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +4 -1
- package/src/index.ts +2 -0
- package/src/layouts/PreviewLayout.astro +14 -1
- package/src/tests/i18n_coverage.test.ts +3 -1
- package/src/tests/seo_translation_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +7 -4
- package/src/tool/aquariumFilterTurnoverStockingPlanner/aquarium-filter-turnover-stocking-planner.css +404 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/bibliography.astro +7 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/bibliography.ts +16 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/component.astro +140 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/content-builder.ts +43 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/controller.ts +94 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/dom-views.ts +63 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/entry.ts +27 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/evaluator.ts +17 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/de.ts +48 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/en.ts +124 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/es.ts +41 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/fr.ts +36 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/id.ts +36 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/it.ts +36 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/ja.ts +36 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/ko.ts +36 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/nl.ts +36 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/pl.ts +36 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/pt.ts +36 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/ru.ts +36 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/sv.ts +36 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/tr.ts +36 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/i18n/zh.ts +36 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/index.ts +11 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/logic.test.ts +78 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/logic.ts +99 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/seo.astro +14 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/storage.ts +21 -0
- package/src/tool/aquariumFilterTurnoverStockingPlanner/ui.ts +40 -0
- package/src/tools.ts +6 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export type AquariumSetup = 'community' | 'planted' | 'highWaste';
|
|
2
|
+
export type StockingLoad = 'light' | 'moderate' | 'heavy';
|
|
3
|
+
export type TurnoverStatus = 'under' | 'target' | 'high';
|
|
4
|
+
|
|
5
|
+
export interface AquariumPlannerInput {
|
|
6
|
+
tankVolumeLiters: number;
|
|
7
|
+
ratedFlowLitersPerHour: number;
|
|
8
|
+
flowLossPercent: number;
|
|
9
|
+
fishCount: number;
|
|
10
|
+
averageAdultLengthCm: number;
|
|
11
|
+
setup: AquariumSetup;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AquariumPlannerResult {
|
|
15
|
+
effectiveFlowLitersPerHour: number;
|
|
16
|
+
ratedTurnover: number;
|
|
17
|
+
effectiveTurnover: number;
|
|
18
|
+
targetMinTurnover: number;
|
|
19
|
+
targetMaxTurnover: number;
|
|
20
|
+
targetMinFlowLitersPerHour: number;
|
|
21
|
+
targetMaxFlowLitersPerHour: number;
|
|
22
|
+
stockingProxyCmPerLiter: number;
|
|
23
|
+
stockingLoad: StockingLoad;
|
|
24
|
+
turnoverStatus: TurnoverStatus;
|
|
25
|
+
flowGapLitersPerHour: number;
|
|
26
|
+
flowAboveTargetLitersPerHour: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SetupProfile {
|
|
30
|
+
min: number;
|
|
31
|
+
max: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SETUP_PROFILES: Record<AquariumSetup, SetupProfile> = {
|
|
35
|
+
community: { min: 4, max: 6 },
|
|
36
|
+
planted: { min: 3, max: 5 },
|
|
37
|
+
highWaste: { min: 6, max: 10 },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function positive(value: number, fallback: number): number {
|
|
41
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function bounded(value: number, min: number, max: number): number {
|
|
45
|
+
return Math.min(max, Math.max(min, Number.isFinite(value) ? value : min));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeInput(input: AquariumPlannerInput): AquariumPlannerInput {
|
|
49
|
+
return {
|
|
50
|
+
tankVolumeLiters: positive(input.tankVolumeLiters, 1),
|
|
51
|
+
ratedFlowLitersPerHour: positive(input.ratedFlowLitersPerHour, 1),
|
|
52
|
+
flowLossPercent: bounded(input.flowLossPercent, 0, 90),
|
|
53
|
+
fishCount: bounded(input.fishCount, 0, 200),
|
|
54
|
+
averageAdultLengthCm: bounded(input.averageAdultLengthCm, 0, 80),
|
|
55
|
+
setup: input.setup in SETUP_PROFILES ? input.setup : 'community',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getStockingLoad(proxy: number): StockingLoad {
|
|
60
|
+
if (proxy <= 0.6) return 'light';
|
|
61
|
+
if (proxy <= 1.2) return 'moderate';
|
|
62
|
+
return 'heavy';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getTurnoverStatus(turnover: number, profile: SetupProfile): TurnoverStatus {
|
|
66
|
+
if (turnover < profile.min) return 'under';
|
|
67
|
+
if (turnover > profile.max) return 'high';
|
|
68
|
+
return 'target';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function calculatePlanner(input: AquariumPlannerInput): AquariumPlannerResult {
|
|
72
|
+
const values = normalizeInput(input);
|
|
73
|
+
const profile = SETUP_PROFILES[values.setup];
|
|
74
|
+
const effectiveFlow = values.ratedFlowLitersPerHour * (1 - values.flowLossPercent / 100);
|
|
75
|
+
const ratedTurnover = values.ratedFlowLitersPerHour / values.tankVolumeLiters;
|
|
76
|
+
const effectiveTurnover = effectiveFlow / values.tankVolumeLiters;
|
|
77
|
+
const stockingProxy = (values.fishCount * values.averageAdultLengthCm) / values.tankVolumeLiters;
|
|
78
|
+
const flowGap = Math.max(0, profile.min * values.tankVolumeLiters - effectiveFlow);
|
|
79
|
+
const flowAbove = Math.max(0, effectiveFlow - profile.max * values.tankVolumeLiters);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
effectiveFlowLitersPerHour: effectiveFlow,
|
|
83
|
+
ratedTurnover,
|
|
84
|
+
effectiveTurnover,
|
|
85
|
+
targetMinTurnover: profile.min,
|
|
86
|
+
targetMaxTurnover: profile.max,
|
|
87
|
+
targetMinFlowLitersPerHour: profile.min * values.tankVolumeLiters,
|
|
88
|
+
targetMaxFlowLitersPerHour: profile.max * values.tankVolumeLiters,
|
|
89
|
+
stockingProxyCmPerLiter: stockingProxy,
|
|
90
|
+
stockingLoad: getStockingLoad(stockingProxy),
|
|
91
|
+
turnoverStatus: getTurnoverStatus(effectiveTurnover, profile),
|
|
92
|
+
flowGapLitersPerHour: flowGap,
|
|
93
|
+
flowAboveTargetLitersPerHour: flowAbove,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getSetupProfile(setup: AquariumSetup): SetupProfile {
|
|
98
|
+
return SETUP_PROFILES[setup];
|
|
99
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { aquariumFilterTurnoverStockingPlanner } from './entry';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props { locale?: KnownLocale; }
|
|
7
|
+
|
|
8
|
+
const { locale = 'en' } = Astro.props;
|
|
9
|
+
const loader = aquariumFilterTurnoverStockingPlanner.i18n[locale] ?? aquariumFilterTurnoverStockingPlanner.i18n.en;
|
|
10
|
+
const content = loader === undefined ? undefined : await loader();
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AquariumPlannerInput } from './logic';
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'jjlmoya-aquarium-filter-turnover-planner';
|
|
4
|
+
|
|
5
|
+
export function loadPlannerState(): AquariumPlannerInput | undefined {
|
|
6
|
+
try {
|
|
7
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
8
|
+
if (!raw) return undefined;
|
|
9
|
+
return JSON.parse(raw) as AquariumPlannerInput;
|
|
10
|
+
} catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function savePlannerState(input: AquariumPlannerInput): void {
|
|
16
|
+
try {
|
|
17
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(input));
|
|
18
|
+
} catch {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface AquariumFilterTurnoverStockingPlannerUI extends Record<string, string> {
|
|
2
|
+
presetsLabel: string;
|
|
3
|
+
presetCommunity: string;
|
|
4
|
+
presetPlanted: string;
|
|
5
|
+
presetHighWaste: string;
|
|
6
|
+
tankVolumeLabel: string;
|
|
7
|
+
tankVolumeHint: string;
|
|
8
|
+
ratedFlowLabel: string;
|
|
9
|
+
ratedFlowHint: string;
|
|
10
|
+
flowLossLabel: string;
|
|
11
|
+
flowLossHint: string;
|
|
12
|
+
fishCountLabel: string;
|
|
13
|
+
averageLengthLabel: string;
|
|
14
|
+
setupLabel: string;
|
|
15
|
+
setupCommunity: string;
|
|
16
|
+
setupPlanted: string;
|
|
17
|
+
setupHighWaste: string;
|
|
18
|
+
resultEyebrow: string;
|
|
19
|
+
effectiveTurnoverLabel: string;
|
|
20
|
+
targetLabel: string;
|
|
21
|
+
effectiveFlowLabel: string;
|
|
22
|
+
stockingProxyLabel: string;
|
|
23
|
+
currentTitle: string;
|
|
24
|
+
currentDescription: string;
|
|
25
|
+
statusUnder: string;
|
|
26
|
+
statusTarget: string;
|
|
27
|
+
statusHigh: string;
|
|
28
|
+
loadLight: string;
|
|
29
|
+
loadModerate: string;
|
|
30
|
+
loadHeavy: string;
|
|
31
|
+
cautionTitle: string;
|
|
32
|
+
bandSignalInside: string;
|
|
33
|
+
bandSignalBelow: string;
|
|
34
|
+
bandSignalAbove: string;
|
|
35
|
+
maintenanceText: string;
|
|
36
|
+
useTestKits: string;
|
|
37
|
+
metricLiters: string;
|
|
38
|
+
metricPerHour: string;
|
|
39
|
+
metricCentimetersPerLiter: string;
|
|
40
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
export { ALL_ENTRIES } from './entries';
|
|
2
2
|
import { aquariumTankVolumeWaterChangeCalculator } from './tool/aquariumTankVolumeWaterChangeCalculator/entry';
|
|
3
|
+
import { aquariumFilterTurnoverStockingPlanner } from './tool/aquariumFilterTurnoverStockingPlanner/entry';
|
|
3
4
|
|
|
4
5
|
export const ALL_TOOLS = [{
|
|
5
6
|
entry: aquariumTankVolumeWaterChangeCalculator,
|
|
6
7
|
Component: () => import('./tool/aquariumTankVolumeWaterChangeCalculator/component.astro'),
|
|
7
8
|
SEOComponent: () => import('./tool/aquariumTankVolumeWaterChangeCalculator/seo.astro'),
|
|
8
9
|
BibliographyComponent: () => import('./tool/aquariumTankVolumeWaterChangeCalculator/bibliography.astro'),
|
|
10
|
+
}, {
|
|
11
|
+
entry: aquariumFilterTurnoverStockingPlanner,
|
|
12
|
+
Component: () => import('./tool/aquariumFilterTurnoverStockingPlanner/component.astro'),
|
|
13
|
+
SEOComponent: () => import('./tool/aquariumFilterTurnoverStockingPlanner/seo.astro'),
|
|
14
|
+
BibliographyComponent: () => import('./tool/aquariumFilterTurnoverStockingPlanner/bibliography.astro'),
|
|
9
15
|
}];
|