@jjlmoya/utils-aquarium 1.58.0 → 1.59.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/tests/i18n_coverage.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +4 -3
- package/src/tool/aquariumLightingPhotoperiodPlanner/aquarium-lighting-photoperiod-planner.css +667 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/bibliography.astro +6 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/bibliography.ts +12 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/component.astro +92 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/controller.ts +179 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/dom-views.test.ts +26 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/dom-views.ts +187 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/entry.ts +29 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/evaluator.ts +47 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/de.ts +125 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/en.ts +125 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/es.ts +110 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/fr.ts +110 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/id.ts +110 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/it.ts +110 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/ja.ts +110 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/ko.ts +110 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/nl.ts +110 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/pl.ts +110 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/pt.ts +110 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/ru.ts +110 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/sv.ts +110 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/tr.ts +110 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/zh.ts +110 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/index.ts +11 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/logic.test.ts +115 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/logic.ts +248 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/seo.astro +13 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/storage.ts +39 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/ui.ts +44 -0
- package/src/tools.ts +6 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
calculateSchedule,
|
|
4
|
+
DEFAULT_INPUT,
|
|
5
|
+
formatHours,
|
|
6
|
+
getLightingBand,
|
|
7
|
+
getLightWindow,
|
|
8
|
+
PRESETS,
|
|
9
|
+
} from "./logic";
|
|
10
|
+
|
|
11
|
+
describe("aquarium lighting photoperiod planner logic", () => {
|
|
12
|
+
it("calculates a planted schedule with transitions and seven days", () => {
|
|
13
|
+
const result = calculateSchedule(DEFAULT_INPUT);
|
|
14
|
+
|
|
15
|
+
expect(result.status).toBe("inside");
|
|
16
|
+
expect(result.band).toEqual({ low: 7, high: 9 });
|
|
17
|
+
expect(result.photoperiodMinutes).toBe(480);
|
|
18
|
+
expect(result.fullIntensityMinutes).toBe(420);
|
|
19
|
+
expect(result.phases.map((phase) => phase.phase)).toEqual([
|
|
20
|
+
"ramp-up",
|
|
21
|
+
"full",
|
|
22
|
+
"ramp-down",
|
|
23
|
+
"dark",
|
|
24
|
+
]);
|
|
25
|
+
expect(result.phases[0]).toMatchObject({
|
|
26
|
+
startTime: "08:00",
|
|
27
|
+
endTime: "08:30",
|
|
28
|
+
duration: 30,
|
|
29
|
+
});
|
|
30
|
+
expect(result.phases[3]).toMatchObject({
|
|
31
|
+
startTime: "16:00",
|
|
32
|
+
endTime: "08:00",
|
|
33
|
+
duration: 960,
|
|
34
|
+
});
|
|
35
|
+
expect(result.days).toHaveLength(7);
|
|
36
|
+
expect(result.days[6]?.label).toBe("sunday");
|
|
37
|
+
expect(getLightWindow(result)).toEqual({
|
|
38
|
+
startTime: "08:00",
|
|
39
|
+
endTime: "16:00",
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("supports schedules that cross midnight and removes zero length transitions", () => {
|
|
44
|
+
const result = calculateSchedule({
|
|
45
|
+
...DEFAULT_INPUT,
|
|
46
|
+
startTime: "20:45",
|
|
47
|
+
photoperiodHours: 6,
|
|
48
|
+
rampUpMinutes: 0,
|
|
49
|
+
rampDownMinutes: 0,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(result.status).toBe("below");
|
|
53
|
+
expect(result.differenceHours).toBe(1);
|
|
54
|
+
expect(result.phases.map((phase) => phase.phase)).toEqual(["full", "dark"]);
|
|
55
|
+
expect(result.phases[0]).toMatchObject({
|
|
56
|
+
startTime: "20:45",
|
|
57
|
+
endTime: "02:45",
|
|
58
|
+
duration: 360,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("caps long transitions so a light cycle always leaves a dark period", () => {
|
|
63
|
+
const result = calculateSchedule({
|
|
64
|
+
...DEFAULT_INPUT,
|
|
65
|
+
photoperiodHours: 6,
|
|
66
|
+
rampUpMinutes: 180,
|
|
67
|
+
rampDownMinutes: 180,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(result.input.rampUpMinutes).toBe(165);
|
|
71
|
+
expect(result.input.rampDownMinutes).toBe(165);
|
|
72
|
+
expect(result.fullIntensityMinutes).toBe(30);
|
|
73
|
+
expect(result.status).toBe("below");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("falls back safely when persisted enum values are invalid", () => {
|
|
77
|
+
const result = calculateSchedule({
|
|
78
|
+
...DEFAULT_INPUT,
|
|
79
|
+
aquariumType: "invalid" as "tropical",
|
|
80
|
+
plantProfile: "invalid" as "low",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(result.input.aquariumType).toBe("tropical");
|
|
84
|
+
expect(result.input.plantProfile).toBe("low");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("reports above-band and inside-band combinations", () => {
|
|
88
|
+
const above = calculateSchedule({
|
|
89
|
+
...DEFAULT_INPUT,
|
|
90
|
+
...PRESETS.marine,
|
|
91
|
+
photoperiodHours: 12,
|
|
92
|
+
});
|
|
93
|
+
const inside = calculateSchedule({
|
|
94
|
+
...DEFAULT_INPUT,
|
|
95
|
+
...PRESETS.marine,
|
|
96
|
+
photoperiodHours: 8,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(above.status).toBe("above");
|
|
100
|
+
expect(above.differenceHours).toBe(4);
|
|
101
|
+
expect(inside.status).toBe("inside");
|
|
102
|
+
expect(inside.differenceHours).toBe(0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("merges aquarium and plant profile bands", () => {
|
|
106
|
+
expect(
|
|
107
|
+
getLightingBand({ aquariumType: "coldwater", plantProfile: "high" }),
|
|
108
|
+
).toEqual({ low: 8, high: 8 });
|
|
109
|
+
expect(
|
|
110
|
+
getLightingBand({ aquariumType: "marine", plantProfile: "none" }),
|
|
111
|
+
).toEqual({ low: 8, high: 8 });
|
|
112
|
+
expect(formatHours(8)).toBe("8");
|
|
113
|
+
expect(formatHours(8.5)).toBe("8.5");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
export type AquariumType = "tropical" | "coldwater" | "marine";
|
|
2
|
+
export type PlantProfile = "none" | "low" | "high";
|
|
3
|
+
export type SchedulePhase = "dark" | "ramp-up" | "full" | "ramp-down";
|
|
4
|
+
export type ScheduleStatus = "below" | "inside" | "above";
|
|
5
|
+
|
|
6
|
+
export interface LightingInput {
|
|
7
|
+
aquariumType: AquariumType;
|
|
8
|
+
plantProfile: PlantProfile;
|
|
9
|
+
startTime: string;
|
|
10
|
+
photoperiodHours: number;
|
|
11
|
+
rampUpMinutes: number;
|
|
12
|
+
rampDownMinutes: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LightingBand {
|
|
16
|
+
low: number;
|
|
17
|
+
high: number;
|
|
18
|
+
}
|
|
19
|
+
export interface PhaseSegment {
|
|
20
|
+
phase: SchedulePhase;
|
|
21
|
+
startOffset: number;
|
|
22
|
+
duration: number;
|
|
23
|
+
startTime: string;
|
|
24
|
+
endTime: string;
|
|
25
|
+
}
|
|
26
|
+
export interface DaySchedule {
|
|
27
|
+
key: string;
|
|
28
|
+
label: string;
|
|
29
|
+
phases: PhaseSegment[];
|
|
30
|
+
}
|
|
31
|
+
export interface LightingSchedule {
|
|
32
|
+
input: LightingInput;
|
|
33
|
+
band: LightingBand;
|
|
34
|
+
status: ScheduleStatus;
|
|
35
|
+
differenceHours: number;
|
|
36
|
+
photoperiodMinutes: number;
|
|
37
|
+
fullIntensityMinutes: number;
|
|
38
|
+
phases: PhaseSegment[];
|
|
39
|
+
days: DaySchedule[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TYPE_BANDS: Record<AquariumType, LightingBand> = {
|
|
43
|
+
tropical: { low: 7, high: 10 },
|
|
44
|
+
coldwater: { low: 6, high: 8 },
|
|
45
|
+
marine: { low: 8, high: 10 },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const PLANT_BANDS: Record<PlantProfile, LightingBand> = {
|
|
49
|
+
none: { low: 6, high: 8 },
|
|
50
|
+
low: { low: 7, high: 9 },
|
|
51
|
+
high: { low: 8, high: 10 },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const DAY_KEYS = [
|
|
55
|
+
"monday",
|
|
56
|
+
"tuesday",
|
|
57
|
+
"wednesday",
|
|
58
|
+
"thursday",
|
|
59
|
+
"friday",
|
|
60
|
+
"saturday",
|
|
61
|
+
"sunday",
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
export const DEFAULT_INPUT: LightingInput = {
|
|
65
|
+
aquariumType: "tropical",
|
|
66
|
+
plantProfile: "low",
|
|
67
|
+
startTime: "08:00",
|
|
68
|
+
photoperiodHours: 8,
|
|
69
|
+
rampUpMinutes: 30,
|
|
70
|
+
rampDownMinutes: 30,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const PRESETS: Record<string, LightingInput> = {
|
|
74
|
+
tropical: {
|
|
75
|
+
...DEFAULT_INPUT,
|
|
76
|
+
aquariumType: "tropical",
|
|
77
|
+
plantProfile: "none",
|
|
78
|
+
photoperiodHours: 7,
|
|
79
|
+
rampUpMinutes: 15,
|
|
80
|
+
rampDownMinutes: 15,
|
|
81
|
+
},
|
|
82
|
+
planted: { ...DEFAULT_INPUT },
|
|
83
|
+
marine: {
|
|
84
|
+
...DEFAULT_INPUT,
|
|
85
|
+
aquariumType: "marine",
|
|
86
|
+
plantProfile: "none",
|
|
87
|
+
startTime: "10:00",
|
|
88
|
+
photoperiodHours: 8,
|
|
89
|
+
rampUpMinutes: 60,
|
|
90
|
+
rampDownMinutes: 60,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function clamp(value: number, low: number, high: number): number {
|
|
95
|
+
return Math.min(Math.max(value, low), high);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseClock(value: string): number {
|
|
99
|
+
const [hourText, minuteText] = value.split(":");
|
|
100
|
+
const hour = Number(hourText);
|
|
101
|
+
const minute = Number(minuteText);
|
|
102
|
+
if (!Number.isFinite(hour) || !Number.isFinite(minute)) return 0;
|
|
103
|
+
return clamp(Math.round(hour) * 60 + Math.round(minute), 0, 1439);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatClock(minutes: number): string {
|
|
107
|
+
const normalized = ((Math.round(minutes) % 1440) + 1440) % 1440;
|
|
108
|
+
const hour = Math.floor(normalized / 60)
|
|
109
|
+
.toString()
|
|
110
|
+
.padStart(2, "0");
|
|
111
|
+
const minute = (normalized % 60).toString().padStart(2, "0");
|
|
112
|
+
return `${hour}:${minute}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeAquariumType(value: string): AquariumType {
|
|
116
|
+
if (value === "coldwater" || value === "marine") return value;
|
|
117
|
+
return "tropical";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizePlantProfile(value: string): PlantProfile {
|
|
121
|
+
if (value === "none" || value === "high") return value;
|
|
122
|
+
return "low";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeInput(input: LightingInput): LightingInput {
|
|
126
|
+
const photoperiodHours = clamp(Number(input.photoperiodHours) || 8, 6, 12);
|
|
127
|
+
const photoperiodMinutes = Math.round(photoperiodHours * 60);
|
|
128
|
+
const rampUpMinutes = clamp(Number(input.rampUpMinutes) || 0, 0, 180);
|
|
129
|
+
const rampDownMinutes = clamp(Number(input.rampDownMinutes) || 0, 0, 180);
|
|
130
|
+
const availableTransition = Math.max(0, photoperiodMinutes - 30);
|
|
131
|
+
const totalTransitions = rampUpMinutes + rampDownMinutes;
|
|
132
|
+
const scale =
|
|
133
|
+
totalTransitions > availableTransition && totalTransitions > 0
|
|
134
|
+
? availableTransition / totalTransitions
|
|
135
|
+
: 1;
|
|
136
|
+
return {
|
|
137
|
+
aquariumType: normalizeAquariumType(input.aquariumType),
|
|
138
|
+
plantProfile: normalizePlantProfile(input.plantProfile),
|
|
139
|
+
startTime: formatClock(parseClock(input.startTime)),
|
|
140
|
+
photoperiodHours,
|
|
141
|
+
rampUpMinutes: Math.round(rampUpMinutes * scale),
|
|
142
|
+
rampDownMinutes: Math.round(rampDownMinutes * scale),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function mergeBands(
|
|
147
|
+
typeBand: LightingBand,
|
|
148
|
+
plantBand: LightingBand,
|
|
149
|
+
): LightingBand {
|
|
150
|
+
const low = Math.max(typeBand.low, plantBand.low);
|
|
151
|
+
const high = Math.min(typeBand.high, plantBand.high);
|
|
152
|
+
return low <= high
|
|
153
|
+
? { low, high }
|
|
154
|
+
: {
|
|
155
|
+
low: Math.min(typeBand.low, plantBand.low),
|
|
156
|
+
high: Math.max(typeBand.high, plantBand.high),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function getLightingBand(
|
|
161
|
+
input: Pick<LightingInput, "aquariumType" | "plantProfile">,
|
|
162
|
+
): LightingBand {
|
|
163
|
+
return mergeBands(
|
|
164
|
+
TYPE_BANDS[input.aquariumType],
|
|
165
|
+
PLANT_BANDS[input.plantProfile],
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getStatus(hours: number, band: LightingBand): ScheduleStatus {
|
|
170
|
+
if (hours < band.low) return "below";
|
|
171
|
+
if (hours > band.high) return "above";
|
|
172
|
+
return "inside";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getDifference(hours: number, band: LightingBand): number {
|
|
176
|
+
if (hours < band.low) return band.low - hours;
|
|
177
|
+
if (hours > band.high) return hours - band.high;
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function makePhase(
|
|
182
|
+
phase: SchedulePhase,
|
|
183
|
+
startOffset: number,
|
|
184
|
+
duration: number,
|
|
185
|
+
startMinutes: number,
|
|
186
|
+
): PhaseSegment {
|
|
187
|
+
return {
|
|
188
|
+
phase,
|
|
189
|
+
startOffset,
|
|
190
|
+
duration,
|
|
191
|
+
startTime: formatClock(startMinutes + startOffset),
|
|
192
|
+
endTime: formatClock(startMinutes + startOffset + duration),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function makePhases(input: LightingInput): PhaseSegment[] {
|
|
197
|
+
const total = input.photoperiodHours * 60;
|
|
198
|
+
const full = total - input.rampUpMinutes - input.rampDownMinutes;
|
|
199
|
+
const start = parseClock(input.startTime);
|
|
200
|
+
return [
|
|
201
|
+
makePhase("ramp-up", 0, input.rampUpMinutes, start),
|
|
202
|
+
makePhase("full", input.rampUpMinutes, full, start),
|
|
203
|
+
makePhase(
|
|
204
|
+
"ramp-down",
|
|
205
|
+
input.rampUpMinutes + full,
|
|
206
|
+
input.rampDownMinutes,
|
|
207
|
+
start,
|
|
208
|
+
),
|
|
209
|
+
makePhase("dark", total, 1440 - total, start),
|
|
210
|
+
].filter((phase) => phase.duration > 0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function makeDays(phases: PhaseSegment[]): DaySchedule[] {
|
|
214
|
+
return DAY_KEYS.map((key) => ({ key, label: key, phases }));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function calculateSchedule(rawInput: LightingInput): LightingSchedule {
|
|
218
|
+
const input = normalizeInput(rawInput);
|
|
219
|
+
const band = getLightingBand(input);
|
|
220
|
+
const phases = makePhases(input);
|
|
221
|
+
const photoperiodMinutes = Math.round(input.photoperiodHours * 60);
|
|
222
|
+
return {
|
|
223
|
+
input,
|
|
224
|
+
band,
|
|
225
|
+
status: getStatus(input.photoperiodHours, band),
|
|
226
|
+
differenceHours: getDifference(input.photoperiodHours, band),
|
|
227
|
+
photoperiodMinutes,
|
|
228
|
+
fullIntensityMinutes:
|
|
229
|
+
photoperiodMinutes - input.rampUpMinutes - input.rampDownMinutes,
|
|
230
|
+
phases,
|
|
231
|
+
days: makeDays(phases),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function formatHours(value: number): string {
|
|
236
|
+
return Number.isInteger(value) ? `${value}` : value.toFixed(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function getLightWindow(schedule: Pick<LightingSchedule, "phases">): {
|
|
240
|
+
startTime: string;
|
|
241
|
+
endTime: string;
|
|
242
|
+
} {
|
|
243
|
+
const lightPhases = schedule.phases.filter((phase) => phase.phase !== "dark");
|
|
244
|
+
return {
|
|
245
|
+
startTime: lightPhases[0]?.startTime ?? "",
|
|
246
|
+
endTime: lightPhases[lightPhases.length - 1]?.endTime ?? "",
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { aquariumLightingPhotoperiodPlanner } from './entry';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props { locale?: KnownLocale; }
|
|
7
|
+
|
|
8
|
+
const { locale = 'en' } = Astro.props;
|
|
9
|
+
const loader = aquariumLightingPhotoperiodPlanner.i18n[locale] ?? aquariumLightingPhotoperiodPlanner.i18n.en;
|
|
10
|
+
const content = loader === undefined ? undefined : await loader();
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
{content && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LightingInput } from "./logic";
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = "jjlmoya-aquarium-lighting-photoperiod-planner";
|
|
4
|
+
|
|
5
|
+
function isLightingInput(value: unknown): value is LightingInput {
|
|
6
|
+
if (!value || typeof value !== "object") return false;
|
|
7
|
+
const candidate = value as Partial<LightingInput>;
|
|
8
|
+
return (
|
|
9
|
+
typeof candidate.aquariumType === "string" &&
|
|
10
|
+
typeof candidate.plantProfile === "string" &&
|
|
11
|
+
typeof candidate.startTime === "string" &&
|
|
12
|
+
typeof candidate.photoperiodHours === "number" &&
|
|
13
|
+
typeof candidate.rampUpMinutes === "number" &&
|
|
14
|
+
typeof candidate.rampDownMinutes === "number"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function loadInput(): LightingInput | undefined {
|
|
19
|
+
try {
|
|
20
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
21
|
+
if (!raw) return undefined;
|
|
22
|
+
const parsed: unknown = JSON.parse(raw);
|
|
23
|
+
return isLightingInput(parsed) ? parsed : undefined;
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function saveInput(input: LightingInput): void {
|
|
30
|
+
try {
|
|
31
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(input));
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function clearInput(): void {
|
|
36
|
+
try {
|
|
37
|
+
window.localStorage.removeItem(STORAGE_KEY);
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface AquariumLightingPhotoperiodPlannerUI extends Record<
|
|
2
|
+
string,
|
|
3
|
+
string
|
|
4
|
+
> {
|
|
5
|
+
controlsLabel: string;
|
|
6
|
+
aquariumTypeLabel: string;
|
|
7
|
+
aquariumTypeTropical: string;
|
|
8
|
+
aquariumTypeColdwater: string;
|
|
9
|
+
aquariumTypeMarine: string;
|
|
10
|
+
plantProfileLabel: string;
|
|
11
|
+
plantProfileNone: string;
|
|
12
|
+
plantProfileLow: string;
|
|
13
|
+
plantProfileHigh: string;
|
|
14
|
+
startTimeLabel: string;
|
|
15
|
+
photoperiodLabel: string;
|
|
16
|
+
rampUpLabel: string;
|
|
17
|
+
rampDownLabel: string;
|
|
18
|
+
optionalRampsLabel: string;
|
|
19
|
+
optionalRampsSummary: string;
|
|
20
|
+
hoursUnit: string;
|
|
21
|
+
minutesUnit: string;
|
|
22
|
+
statusBelow: string;
|
|
23
|
+
statusInside: string;
|
|
24
|
+
statusAbove: string;
|
|
25
|
+
targetBandLabel: string;
|
|
26
|
+
lightWindowLabel: string;
|
|
27
|
+
fullIntensityLabel: string;
|
|
28
|
+
lightsOnLabel: string;
|
|
29
|
+
fullLightLabel: string;
|
|
30
|
+
lightsOffLabel: string;
|
|
31
|
+
dayNightLabel: string;
|
|
32
|
+
fullDayLabel: string;
|
|
33
|
+
dailyRepeatLabel: string;
|
|
34
|
+
co2Label: string;
|
|
35
|
+
co2Hint: string;
|
|
36
|
+
phaseDark: string;
|
|
37
|
+
phaseRampUp: string;
|
|
38
|
+
phaseFull: string;
|
|
39
|
+
phaseRampDown: string;
|
|
40
|
+
adjustmentBelow: string;
|
|
41
|
+
adjustmentInside: string;
|
|
42
|
+
adjustmentAbove: string;
|
|
43
|
+
limitsText: string;
|
|
44
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { ALL_ENTRIES } from './entries';
|
|
2
2
|
import { aquariumTankVolumeWaterChangeCalculator } from './tool/aquariumTankVolumeWaterChangeCalculator/entry';
|
|
3
3
|
import { aquariumFilterTurnoverStockingPlanner } from './tool/aquariumFilterTurnoverStockingPlanner/entry';
|
|
4
|
+
import { aquariumLightingPhotoperiodPlanner } from './tool/aquariumLightingPhotoperiodPlanner/entry';
|
|
4
5
|
|
|
5
6
|
export const ALL_TOOLS = [{
|
|
6
7
|
entry: aquariumTankVolumeWaterChangeCalculator,
|
|
@@ -12,4 +13,9 @@ export const ALL_TOOLS = [{
|
|
|
12
13
|
Component: () => import('./tool/aquariumFilterTurnoverStockingPlanner/component.astro'),
|
|
13
14
|
SEOComponent: () => import('./tool/aquariumFilterTurnoverStockingPlanner/seo.astro'),
|
|
14
15
|
BibliographyComponent: () => import('./tool/aquariumFilterTurnoverStockingPlanner/bibliography.astro'),
|
|
16
|
+
}, {
|
|
17
|
+
entry: aquariumLightingPhotoperiodPlanner,
|
|
18
|
+
Component: () => import('./tool/aquariumLightingPhotoperiodPlanner/component.astro'),
|
|
19
|
+
SEOComponent: () => import('./tool/aquariumLightingPhotoperiodPlanner/seo.astro'),
|
|
20
|
+
BibliographyComponent: () => import('./tool/aquariumLightingPhotoperiodPlanner/bibliography.astro'),
|
|
15
21
|
}];
|