@jjlmoya/utils-books 1.7.0 → 1.9.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 +7 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/book-index-page-budget-calculator/bibliography.astro +6 -0
- package/src/tool/book-index-page-budget-calculator/bibliography.ts +17 -0
- package/src/tool/book-index-page-budget-calculator/book-index-page-budget-calculator.css +478 -0
- package/src/tool/book-index-page-budget-calculator/component.astro +134 -0
- package/src/tool/book-index-page-budget-calculator/controller.ts +114 -0
- package/src/tool/book-index-page-budget-calculator/dom-views.ts +89 -0
- package/src/tool/book-index-page-budget-calculator/entry.ts +39 -0
- package/src/tool/book-index-page-budget-calculator/evaluator.ts +26 -0
- package/src/tool/book-index-page-budget-calculator/i18n/de.ts +35 -0
- package/src/tool/book-index-page-budget-calculator/i18n/en.ts +113 -0
- package/src/tool/book-index-page-budget-calculator/i18n/es.ts +35 -0
- package/src/tool/book-index-page-budget-calculator/i18n/fr.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/id.ts +35 -0
- package/src/tool/book-index-page-budget-calculator/i18n/it.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/ja.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/ko.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/nl.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/pl.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/pt.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/ru.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/sv.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/tr.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/i18n/zh.ts +8 -0
- package/src/tool/book-index-page-budget-calculator/index.ts +11 -0
- package/src/tool/book-index-page-budget-calculator/logic.test.ts +52 -0
- package/src/tool/book-index-page-budget-calculator/logic.ts +86 -0
- package/src/tool/book-index-page-budget-calculator/seo.astro +15 -0
- package/src/tool/book-index-page-budget-calculator/storage.ts +17 -0
- package/src/tool/book-index-page-budget-calculator/ui.ts +41 -0
- package/src/tool/book-reading-time-deadline-planner/bibliography.astro +16 -0
- package/src/tool/book-reading-time-deadline-planner/bibliography.ts +31 -0
- package/src/tool/book-reading-time-deadline-planner/book-reading-time-deadline-planner.css +449 -0
- package/src/tool/book-reading-time-deadline-planner/calendar.test.ts +18 -0
- package/src/tool/book-reading-time-deadline-planner/calendar.ts +56 -0
- package/src/tool/book-reading-time-deadline-planner/component.astro +91 -0
- package/src/tool/book-reading-time-deadline-planner/controller.ts +253 -0
- package/src/tool/book-reading-time-deadline-planner/dom-views.ts +60 -0
- package/src/tool/book-reading-time-deadline-planner/entry.ts +27 -0
- package/src/tool/book-reading-time-deadline-planner/evaluator.ts +17 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/de.ts +45 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/en.ts +51 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/es.ts +45 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/fr.ts +45 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/id.ts +45 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/it.ts +45 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/ja.ts +45 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/ko.ts +45 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/nl.ts +45 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/pl.ts +45 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/pt.ts +45 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/ru.ts +45 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/sv.ts +45 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/tr.ts +45 -0
- package/src/tool/book-reading-time-deadline-planner/i18n/zh.ts +45 -0
- package/src/tool/book-reading-time-deadline-planner/index.ts +11 -0
- package/src/tool/book-reading-time-deadline-planner/logic.test.ts +61 -0
- package/src/tool/book-reading-time-deadline-planner/logic.ts +129 -0
- package/src/tool/book-reading-time-deadline-planner/seo.astro +15 -0
- package/src/tool/book-reading-time-deadline-planner/speed-test.test.ts +15 -0
- package/src/tool/book-reading-time-deadline-planner/speed-test.ts +9 -0
- package/src/tool/book-reading-time-deadline-planner/storage.ts +24 -0
- package/src/tool/book-reading-time-deadline-planner/ui.ts +76 -0
- package/src/tool/book-reading-time-deadline-planner/validation.ts +19 -0
- package/src/tools.ts +3 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { calculateReadingPlan, getDefaultInputs, type BookReadingInputs, type ReadingMode } from './logic';
|
|
2
|
+
import { createMilestoneIcs } from './calendar';
|
|
3
|
+
import { renderReadingPlan } from './dom-views';
|
|
4
|
+
import { loadBookReadingState, saveBookReadingState } from './storage';
|
|
5
|
+
import { calculateWordsPerMinute, countReadingWords } from './speed-test';
|
|
6
|
+
import type { BookReadingUI } from './ui';
|
|
7
|
+
|
|
8
|
+
interface ReadingState {
|
|
9
|
+
inputs: BookReadingInputs;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function initBookReadingPlanner(root: HTMLElement, ui: BookReadingUI): void {
|
|
13
|
+
const defaultInputs = getDefaultInputs();
|
|
14
|
+
const stored = loadBookReadingState({ inputs: defaultInputs });
|
|
15
|
+
const state: ReadingState = { inputs: { ...defaultInputs, ...stored.inputs } };
|
|
16
|
+
bindModeSelect(root, state, ui);
|
|
17
|
+
bindInputs(root, state, ui);
|
|
18
|
+
bindPresets(root, state, ui);
|
|
19
|
+
bindSpeedTest(root, state, ui);
|
|
20
|
+
bindCalendarExport(root, state, ui);
|
|
21
|
+
updateView(root, state, ui);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function bindInputs(root: HTMLElement, state: ReadingState, ui: BookReadingUI): void {
|
|
25
|
+
root.querySelectorAll<HTMLInputElement | HTMLSelectElement>('[data-field]').forEach((input) => {
|
|
26
|
+
const eventName = input instanceof HTMLSelectElement ? 'change' : 'input';
|
|
27
|
+
input.addEventListener(eventName, () => {
|
|
28
|
+
const field = input.dataset.field as keyof BookReadingInputs;
|
|
29
|
+
const isNumeric = input instanceof HTMLSelectElement || input.type === 'number';
|
|
30
|
+
state.inputs[field] = (isNumeric ? Number(input.value) : input.value) as never;
|
|
31
|
+
updateView(root, state, ui);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function bindModeSelect(root: HTMLElement, state: ReadingState, ui: BookReadingUI): void {
|
|
37
|
+
const select = root.querySelector<HTMLElement>('[data-select="mode"]');
|
|
38
|
+
const trigger = select?.querySelector<HTMLButtonElement>('[data-select-trigger]');
|
|
39
|
+
const menu = select?.querySelector<HTMLElement>('[data-select-menu]');
|
|
40
|
+
if (!select || !trigger || !menu) return;
|
|
41
|
+
trigger.addEventListener('click', () => {
|
|
42
|
+
const isOpen = select.dataset.open === 'true';
|
|
43
|
+
closeModeSelect(select, menu);
|
|
44
|
+
if (!isOpen) {
|
|
45
|
+
select.dataset.open = 'true';
|
|
46
|
+
menu.hidden = false;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
select.querySelectorAll<HTMLButtonElement>('[data-select-option]').forEach((option) => {
|
|
50
|
+
option.addEventListener('click', () => {
|
|
51
|
+
const nextMode = option.dataset.value as ReadingMode;
|
|
52
|
+
adaptModeDefaults(state.inputs, nextMode);
|
|
53
|
+
closeModeSelect(select, menu);
|
|
54
|
+
updateView(root, state, ui);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
document.addEventListener('click', (event) => {
|
|
58
|
+
if (!(event.target instanceof Node) || !root.contains(event.target)) closeModeSelect(select, menu);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function adaptModeDefaults(inputs: BookReadingInputs, nextMode: ReadingMode): void {
|
|
63
|
+
if (inputs.mode === nextMode) return;
|
|
64
|
+
const previousDefaults = inputs.mode === 'pages' ? { amount: 320, speed: 35 } : { amount: 60000, speed: 250 };
|
|
65
|
+
const nextDefaults = nextMode === 'pages' ? { amount: 320, speed: 35 } : { amount: 60000, speed: 250 };
|
|
66
|
+
if (inputs.amount === previousDefaults.amount) inputs.amount = nextDefaults.amount;
|
|
67
|
+
if (inputs.speed === previousDefaults.speed) inputs.speed = nextDefaults.speed;
|
|
68
|
+
inputs.mode = nextMode;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function bindPresets(root: HTMLElement, state: ReadingState, ui: BookReadingUI): void {
|
|
72
|
+
const presets: Record<string, { inputs: Partial<BookReadingInputs> }> = {
|
|
73
|
+
steady: { inputs: { sessionMinutes: 30, sessionsPerDay: 1, daysPerWeek: 5 } },
|
|
74
|
+
weekend: { inputs: { sessionMinutes: 45, sessionsPerDay: 2, daysPerWeek: 2 } },
|
|
75
|
+
academic: { inputs: { mode: 'words', amount: 60000, speed: 250, sessionMinutes: 45, sessionsPerDay: 2, daysPerWeek: 6 } },
|
|
76
|
+
};
|
|
77
|
+
root.querySelectorAll<HTMLButtonElement>('[data-preset]').forEach((button) => {
|
|
78
|
+
button.addEventListener('click', () => {
|
|
79
|
+
const preset = presets[button.dataset.preset || ''];
|
|
80
|
+
if (!preset) return;
|
|
81
|
+
state.inputs = { ...state.inputs, ...preset.inputs };
|
|
82
|
+
updateView(root, state, ui);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function bindSpeedTest(root: HTMLElement, state: ReadingState, ui: BookReadingUI): void {
|
|
88
|
+
const elements = getSpeedTestElements(root);
|
|
89
|
+
if (!elements) return;
|
|
90
|
+
const speedState: SpeedTestState = { startedAt: 0, timer: undefined, measuredRate: 0 };
|
|
91
|
+
elements.action.addEventListener('click', () => {
|
|
92
|
+
if (speedState.startedAt) finishSpeedTest(elements, speedState, ui);
|
|
93
|
+
else startSpeedTest(elements, speedState, ui);
|
|
94
|
+
});
|
|
95
|
+
elements.useResult.addEventListener('click', () => applySpeedTest({ elements, speedState, state, root, ui }));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function bindCalendarExport(root: HTMLElement, state: ReadingState, ui: BookReadingUI): void {
|
|
99
|
+
const button = root.querySelector<HTMLButtonElement>('[data-export-ics]');
|
|
100
|
+
if (!button) return;
|
|
101
|
+
button.addEventListener('click', () => downloadMilestones(state, ui));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function downloadMilestones(state: ReadingState, ui: BookReadingUI): void {
|
|
105
|
+
clampNumericInputs(state.inputs);
|
|
106
|
+
const calculation = calculateReadingPlan(state.inputs);
|
|
107
|
+
const ics = createMilestoneIcs(calculation, ui.exportCalendarTitle, {
|
|
108
|
+
start: ui.milestoneStart,
|
|
109
|
+
quarter: ui.milestoneQuarter,
|
|
110
|
+
half: ui.milestoneHalf,
|
|
111
|
+
threeQuarter: ui.milestoneThreeQuarter,
|
|
112
|
+
finish: ui.milestoneFinish,
|
|
113
|
+
});
|
|
114
|
+
const link = document.createElement('a');
|
|
115
|
+
const url = URL.createObjectURL(new Blob([ics], { type: 'text/calendar;charset=utf-8' }));
|
|
116
|
+
link.href = url;
|
|
117
|
+
link.download = `reading-milestones-${calculation.startDate}.ics`;
|
|
118
|
+
document.body.append(link);
|
|
119
|
+
link.click();
|
|
120
|
+
link.remove();
|
|
121
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface SpeedTestElements {
|
|
125
|
+
text: HTMLTextAreaElement;
|
|
126
|
+
action: HTMLButtonElement;
|
|
127
|
+
useResult: HTMLButtonElement;
|
|
128
|
+
status: HTMLElement;
|
|
129
|
+
elapsed: HTMLElement;
|
|
130
|
+
rate: HTMLElement;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface SpeedTestState {
|
|
134
|
+
startedAt: number;
|
|
135
|
+
timer: ReturnType<typeof setInterval> | undefined;
|
|
136
|
+
measuredRate: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getSpeedTestElements(root: HTMLElement): SpeedTestElements | null {
|
|
140
|
+
const elements = {
|
|
141
|
+
text: root.querySelector<HTMLTextAreaElement>('[data-speed-test-text]'),
|
|
142
|
+
action: root.querySelector<HTMLButtonElement>('[data-speed-test-action]'),
|
|
143
|
+
useResult: root.querySelector<HTMLButtonElement>('[data-speed-test-use]'),
|
|
144
|
+
status: root.querySelector<HTMLElement>('[data-speed-test-status]'),
|
|
145
|
+
elapsed: root.querySelector<HTMLElement>('[data-speed-test-elapsed]'),
|
|
146
|
+
rate: root.querySelector<HTMLElement>('[data-speed-test-rate]'),
|
|
147
|
+
};
|
|
148
|
+
if (Object.values(elements).some((element) => !element)) return null;
|
|
149
|
+
return elements as SpeedTestElements;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function startSpeedTest(elements: SpeedTestElements, speedState: SpeedTestState, ui: BookReadingUI): void {
|
|
153
|
+
if (!countReadingWords(elements.text.value)) {
|
|
154
|
+
elements.status.textContent = ui.speedTestNeedsText;
|
|
155
|
+
elements.text.focus();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
speedState.startedAt = Date.now();
|
|
159
|
+
speedState.measuredRate = 0;
|
|
160
|
+
elements.elapsed.textContent = formatSeconds(0, ui);
|
|
161
|
+
elements.rate.textContent = '-';
|
|
162
|
+
elements.useResult.hidden = true;
|
|
163
|
+
elements.action.textContent = ui.speedTestFinishLabel;
|
|
164
|
+
elements.action.classList.add('is-running');
|
|
165
|
+
elements.status.textContent = ui.speedTestRunning;
|
|
166
|
+
speedState.timer = setInterval(() => updateSpeedTestElapsed(elements, speedState, ui), 250);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function updateSpeedTestElapsed(elements: SpeedTestElements, speedState: SpeedTestState, ui: BookReadingUI): void {
|
|
170
|
+
if (speedState.startedAt) {
|
|
171
|
+
elements.elapsed.textContent = formatSeconds((Date.now() - speedState.startedAt) / 1000, ui);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function finishSpeedTest(elements: SpeedTestElements, speedState: SpeedTestState, ui: BookReadingUI): void {
|
|
176
|
+
if (!speedState.startedAt) return;
|
|
177
|
+
const seconds = Math.max(0.1, (Date.now() - speedState.startedAt) / 1000);
|
|
178
|
+
speedState.measuredRate = calculateWordsPerMinute(countReadingWords(elements.text.value), seconds);
|
|
179
|
+
elements.elapsed.textContent = formatSeconds(seconds, ui);
|
|
180
|
+
elements.rate.textContent = speedState.measuredRate ? `${speedState.measuredRate} ${ui.speedTestWpmLabel}` : ui.speedTestNoWords;
|
|
181
|
+
elements.status.textContent = ui.speedTestFinished;
|
|
182
|
+
elements.useResult.hidden = !speedState.measuredRate;
|
|
183
|
+
elements.action.textContent = ui.speedTestStartLabel;
|
|
184
|
+
elements.action.classList.remove('is-running');
|
|
185
|
+
clearInterval(speedState.timer);
|
|
186
|
+
speedState.timer = undefined;
|
|
187
|
+
speedState.startedAt = 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface SpeedTestContext {
|
|
191
|
+
elements: SpeedTestElements;
|
|
192
|
+
speedState: SpeedTestState;
|
|
193
|
+
state: ReadingState;
|
|
194
|
+
root: HTMLElement;
|
|
195
|
+
ui: BookReadingUI;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function applySpeedTest(context: SpeedTestContext): void {
|
|
199
|
+
context.state.inputs.mode = 'words';
|
|
200
|
+
context.state.inputs.speed = context.speedState.measuredRate;
|
|
201
|
+
updateView(context.root, context.state, context.ui);
|
|
202
|
+
context.elements.status.textContent = context.ui.speedTestFinished;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function updateView(root: HTMLElement, state: ReadingState, ui: BookReadingUI): void {
|
|
206
|
+
clampNumericInputs(state.inputs);
|
|
207
|
+
const calculation = calculateReadingPlan(state.inputs);
|
|
208
|
+
updateInputs(root, state.inputs);
|
|
209
|
+
updateMode(root, state.inputs.mode, ui);
|
|
210
|
+
renderReadingPlan(root, calculation, ui);
|
|
211
|
+
saveBookReadingState({ inputs: state.inputs });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function clampNumericInputs(inputs: BookReadingInputs): void {
|
|
215
|
+
inputs.amount = Math.min(10000000, Math.max(1, Number.isFinite(inputs.amount) ? inputs.amount : 1));
|
|
216
|
+
const defaultSpeed = inputs.mode === 'words' ? 250 : 35;
|
|
217
|
+
inputs.speed = Math.min(5000, Math.max(1, Number.isFinite(inputs.speed) ? inputs.speed : defaultSpeed));
|
|
218
|
+
inputs.sessionMinutes = Math.min(240, Math.max(5, Number.isFinite(inputs.sessionMinutes) ? inputs.sessionMinutes : 30));
|
|
219
|
+
inputs.sessionsPerDay = Math.min(8, Math.max(1, Math.round(Number.isFinite(inputs.sessionsPerDay) ? inputs.sessionsPerDay : 1)));
|
|
220
|
+
inputs.daysPerWeek = Math.min(7, Math.max(1, Math.round(Number.isFinite(inputs.daysPerWeek) ? inputs.daysPerWeek : 5)));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function updateInputs(root: HTMLElement, inputs: BookReadingInputs): void {
|
|
224
|
+
root.querySelectorAll<HTMLInputElement | HTMLSelectElement>('[data-field]').forEach((input) => {
|
|
225
|
+
const field = input.dataset.field as keyof BookReadingInputs;
|
|
226
|
+
input.value = String(inputs[field]);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function updateMode(root: HTMLElement, mode: ReadingMode, ui: BookReadingUI): void {
|
|
231
|
+
const labels: Record<ReadingMode, string> = { pages: ui.pagesModeLabel, words: ui.wordsModeLabel };
|
|
232
|
+
const select = root.querySelector<HTMLElement>('[data-select="mode"]');
|
|
233
|
+
const trigger = select?.querySelector('[data-select-trigger]');
|
|
234
|
+
if (trigger) trigger.textContent = labels[mode];
|
|
235
|
+
root.querySelectorAll<HTMLElement>('[data-speed-label]').forEach((label) => {
|
|
236
|
+
label.textContent = mode === 'pages' ? ui.pagesPerHourLabel : ui.wordsPerMinuteLabel;
|
|
237
|
+
});
|
|
238
|
+
root.querySelectorAll<HTMLElement>('[data-amount-label]').forEach((label) => {
|
|
239
|
+
label.textContent = mode === 'pages' ? ui.pagesModeLabel : ui.wordsModeLabel;
|
|
240
|
+
});
|
|
241
|
+
root.querySelectorAll<HTMLElement>('[data-pages-only], [data-pages-output]').forEach((element) => {
|
|
242
|
+
element.hidden = mode !== 'pages';
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function closeModeSelect(select: HTMLElement, menu: HTMLElement): void {
|
|
247
|
+
select.dataset.open = 'false';
|
|
248
|
+
menu.hidden = true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function formatSeconds(seconds: number, ui: BookReadingUI): string {
|
|
252
|
+
return `${seconds.toFixed(1)} ${ui.speedTestSecondsLabel}`;
|
|
253
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { BookReadingCalculation } from './logic';
|
|
2
|
+
import type { BookReadingUI } from './ui';
|
|
3
|
+
import { evaluateReadingPlan } from './evaluator';
|
|
4
|
+
|
|
5
|
+
export function createTimelineMarkup(calculation: BookReadingCalculation, ui: BookReadingUI): string {
|
|
6
|
+
const evaluation = evaluateReadingPlan(calculation);
|
|
7
|
+
const labels = [ui.milestoneStart, ui.milestoneQuarter, ui.milestoneHalf, ui.milestoneThreeQuarter, ui.milestoneFinish];
|
|
8
|
+
const points = calculation.milestoneDates.map((date, index) => {
|
|
9
|
+
const x = 22 + index * 69;
|
|
10
|
+
return `<g class="n-milestone"><circle cx="${x}" cy="72" r="7" /><text x="${x}" y="52" text-anchor="middle">${labels[index]}</text><text x="${x}" y="101" text-anchor="middle">${formatDate(date)}</text></g>`;
|
|
11
|
+
}).join('');
|
|
12
|
+
return `<svg class="n-timeline-svg" viewBox="0 0 320 118" role="img" aria-label="${ui.timelineLabel}"><path class="n-timeline-track" d="M22 72 H298" /><path class="n-timeline-fill n-timeline-fill-${evaluation.status}" d="M22 72 H${calculation.status === 'late' ? 160 : 298}" />${points}</svg>`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function renderReadingPlan(root: HTMLElement, calculation: BookReadingCalculation, ui: BookReadingUI): void {
|
|
16
|
+
const evaluation = evaluateReadingPlan(calculation);
|
|
17
|
+
root.dataset.status = evaluation.status;
|
|
18
|
+
const status = root.querySelector<HTMLElement>('.n-status');
|
|
19
|
+
if (status) status.dataset.status = evaluation.status;
|
|
20
|
+
const values: Record<string, string> = {
|
|
21
|
+
time: formatDuration(calculation.minutesNeeded, ui),
|
|
22
|
+
days: String(calculation.readingDaysNeeded),
|
|
23
|
+
capacity: String(calculation.availableReadingDays),
|
|
24
|
+
pace: `${formatNumber(calculation.dailyPace)} ${getModeLabel(calculation.mode, ui)}`,
|
|
25
|
+
estimatedWords: calculation.estimatedWords === null ? '-' : `${formatNumber(calculation.estimatedWords)} ${ui.wordsShortLabel}`,
|
|
26
|
+
session: `${formatNumber(calculation.sessionTarget)} ${getModeLabel(calculation.mode, ui)}`,
|
|
27
|
+
catchUp: calculation.sessionsShort > 0 ? `${calculation.sessionsShort} ${ui.bufferShortLabel}` : `${calculation.catchUpSessions} ${ui.bufferExtraLabel}`,
|
|
28
|
+
finish: formatDate(calculation.finishDate),
|
|
29
|
+
};
|
|
30
|
+
Object.entries(values).forEach(([key, value]) => {
|
|
31
|
+
const target = root.querySelector(`[data-output="${key}"]`);
|
|
32
|
+
if (target) target.textContent = value;
|
|
33
|
+
});
|
|
34
|
+
const statusTitle = root.querySelector('[data-status-title]');
|
|
35
|
+
const statusDetail = root.querySelector('[data-status-detail]');
|
|
36
|
+
if (statusTitle) statusTitle.textContent = ui[evaluation.messageKey];
|
|
37
|
+
if (statusDetail) statusDetail.textContent = ui[evaluation.detailKey];
|
|
38
|
+
const timeline = root.querySelector('[data-timeline]');
|
|
39
|
+
if (timeline) timeline.innerHTML = createTimelineMarkup(calculation, ui);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatNumber(value: number): string {
|
|
43
|
+
return new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatDuration(minutes: number, ui: BookReadingUI): string {
|
|
47
|
+
const hours = Math.floor(minutes / 60);
|
|
48
|
+
const remaining = Math.round(minutes % 60);
|
|
49
|
+
if (hours === 0) return `${remaining} ${ui.minuteShortLabel}`;
|
|
50
|
+
if (remaining === 0) return `${hours} ${ui.hourShortLabel}`;
|
|
51
|
+
return `${hours} ${ui.hourShortLabel} ${remaining} ${ui.minuteShortLabel}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getModeLabel(mode: BookReadingCalculation['mode'], ui: BookReadingUI): string {
|
|
55
|
+
return mode === 'pages' ? ui.pagesModeLabel : ui.wordsModeLabel;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatDate(value: string): string {
|
|
59
|
+
return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(new Date(`${value}T00:00:00Z`));
|
|
60
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { BooksToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { BookReadingUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type { BookReadingUI } from './ui';
|
|
5
|
+
export type BookReadingLocaleContent = ToolLocaleContent<BookReadingUI>;
|
|
6
|
+
|
|
7
|
+
export const bookReadingTimeDeadlinePlanner: BooksToolEntry<BookReadingUI> = {
|
|
8
|
+
id: 'book-reading-time-deadline-planner',
|
|
9
|
+
icons: { bg: 'mdi:book-clock-outline', fg: 'mdi:calendar-check-outline' },
|
|
10
|
+
i18n: {
|
|
11
|
+
de: () => import('./i18n/de').then((module) => module.content),
|
|
12
|
+
en: () => import('./i18n/en').then((module) => module.content),
|
|
13
|
+
es: () => import('./i18n/es').then((module) => module.content),
|
|
14
|
+
fr: () => import('./i18n/fr').then((module) => module.content),
|
|
15
|
+
id: () => import('./i18n/id').then((module) => module.content),
|
|
16
|
+
it: () => import('./i18n/it').then((module) => module.content),
|
|
17
|
+
ja: () => import('./i18n/ja').then((module) => module.content),
|
|
18
|
+
ko: () => import('./i18n/ko').then((module) => module.content),
|
|
19
|
+
nl: () => import('./i18n/nl').then((module) => module.content),
|
|
20
|
+
pl: () => import('./i18n/pl').then((module) => module.content),
|
|
21
|
+
pt: () => import('./i18n/pt').then((module) => module.content),
|
|
22
|
+
ru: () => import('./i18n/ru').then((module) => module.content),
|
|
23
|
+
sv: () => import('./i18n/sv').then((module) => module.content),
|
|
24
|
+
tr: () => import('./i18n/tr').then((module) => module.content),
|
|
25
|
+
zh: () => import('./i18n/zh').then((module) => module.content),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BookReadingCalculation, ReadingStatus } from './logic';
|
|
2
|
+
|
|
3
|
+
export interface ReadingEvaluation {
|
|
4
|
+
status: ReadingStatus;
|
|
5
|
+
messageKey: 'statusOnTrack' | 'statusTight' | 'statusLate' | 'statusInvalid';
|
|
6
|
+
detailKey: 'statusOnTrackDetail' | 'statusTightDetail' | 'statusLateDetail' | 'statusInvalidDetail';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function evaluateReadingPlan(calculation: BookReadingCalculation): ReadingEvaluation {
|
|
10
|
+
const keys: Record<ReadingStatus, { messageKey: ReadingEvaluation['messageKey']; detailKey: ReadingEvaluation['detailKey'] }> = {
|
|
11
|
+
'on-track': { messageKey: 'statusOnTrack', detailKey: 'statusOnTrackDetail' },
|
|
12
|
+
tight: { messageKey: 'statusTight', detailKey: 'statusTightDetail' },
|
|
13
|
+
late: { messageKey: 'statusLate', detailKey: 'statusLateDetail' },
|
|
14
|
+
invalid: { messageKey: 'statusInvalid', detailKey: 'statusInvalidDetail' },
|
|
15
|
+
};
|
|
16
|
+
return { status: calculation.status, ...keys[calculation.status] };
|
|
17
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { BookReadingUI } from '../ui';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
|
|
5
|
+
|
|
6
|
+
const ui: BookReadingUI = {
|
|
7
|
+
setupLabel: 'Leseplan erstellen', modeLabel: 'Zaehlen nach', pagesModeLabel: 'Seiten', wordsModeLabel: 'Woerter', amountLabel: 'Gesamtumfang', speedLabel: 'Deine Lesegeschwindigkeit', difficultyLabel: 'Textschwierigkeit', difficultyLightLabel: 'Leicht · 1.2x', difficultyStandardLabel: 'Standard · 1.0x', difficultyDenseLabel: 'Dicht · 0.8x', wordsPerPageLabel: 'Geschaetzte Woerter pro Seite', wordsPerPageHint: 'Typischer Bereich: 100 bis 500', pagesPerHourLabel: 'Seiten pro Stunde', wordsPerMinuteLabel: 'Woerter pro Minute', startDateLabel: 'Startdatum', deadlineLabel: 'Deadline', daysPerWeekLabel: 'Lesetage pro Woche', sessionsPerDayLabel: 'Sitzungen pro Lesetag', sessionMinutesLabel: 'Minuten pro Sitzung', minutesRangeLabel: '5 bis 240 Min.', hourShortLabel: 'Std.', minuteShortLabel: 'Min.', bufferShortLabel: 'zu wenig', bufferExtraLabel: 'zusaetzlich', speedTestLabel: 'Lesegeschwindigkeit messen', speedTestHint: 'Verwende einen kurzen, passenden Text', speedTestPromptLabel: 'Zu lesender Text', speedTestPlaceholder: 'Fuege hier einen Satz oder kurzen Text ein ...', speedTestStartLabel: 'Timer starten', speedTestFinishLabel: 'Fertig gelesen', speedTestElapsedLabel: 'Vergangen', speedTestRateLabel: 'Geschaetztes Tempo', speedTestSecondsLabel: 'Sek.', speedTestWpmLabel: 'WPM', speedTestUseLabel: 'Diese WPM im Woertermodus verwenden', speedTestIdle: 'Gib einen Text ein und starte den Timer, wenn du bereit bist.', speedTestRunning: 'Timer laeuft. Lies in deinem natuerlichen Tempo.', speedTestFinished: 'Test beendet. Dies ist eine erste Schaetzung, kein Richtwert.', speedTestNeedsText: 'Fuege vor dem Start einen Text ein.', speedTestNoWords: 'Keine Woerter gemessen', presetLabel: 'Schneller Rhythmus', presetSteadyLabel: 'Gleichmaessige Woche', presetWeekendLabel: 'Wochenendfokus', presetAcademicLabel: 'Lernintensiv', planLabel: 'Deine Lesekarte', timeNeededLabel: 'Benoetigte Zeit', readingDaysLabel: 'Benoetigte Lesetage', capacityLabel: 'Verfuegbare Lesetage', dailyPaceLabel: 'Tempo bis zur Deadline', estimatedWordsLabel: 'Geschaetzte Woerter', wordsShortLabel: 'Woerter', sessionTargetLabel: 'Ziel pro Sitzung', catchUpLabel: 'Aufholpuffer', finishDateLabel: 'Voraussichtliches Ende', milestoneLabel: 'Meilensteine', timelineLabel: 'Zeitplan der Lesemeilensteine', calendarHint: 'Meilensteine sind Planungsmarken. Verschiebe die Deadline oder passe deinen Rhythmus an, wenn das echte Leben dazwischenkommt.', exportCalendarLabel: 'Meilensteine herunterladen (.ics)', exportCalendarTitle: 'Lesemeilensteine', milestoneStart: 'Start', milestoneQuarter: '25 %', milestoneHalf: 'Haelfte', milestoneThreeQuarter: '75 %', milestoneFinish: 'Ende', statusOnTrack: 'Luft im Plan', statusTight: 'Knapp passend', statusLate: 'Nach der Deadline', statusInvalid: 'Daten pruefen', statusOnTrackDetail: 'Deine verfuegbaren Sitzungen lassen Puffer fuer ausgefallene Tage oder ein langsameres Kapitel.', statusTightDetail: 'Der Plan passt, hat aber wenig Reserve. Halte eine Sitzung flexibel zum Aufholen.', statusLateDetail: 'Der aktuelle Rhythmus reicht nicht bis zur Deadline. Fuege Sitzungen hinzu, verlaengere den Zeitraum oder reduziere das Ziel.', statusInvalidDetail: 'Waehle ein gueltiges Startdatum und eine Deadline am oder nach dem Startdatum.', sourceLabel: 'Dies ist eine Schaetzung des Tempos, keine Zusage fuer Verstaendnis und keine feste Lesegeschwindigkeit. Miss dein eigenes Tempo an einem passenden Text.',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const faq = [
|
|
11
|
+
{ question: 'Wie waehle ich meine Lesegeschwindigkeit?', answer: 'Stoppe die Zeit fuer einen passenden Text, zaehle Seiten oder Woerter und verwende das beobachtete Tempo. Dichte, technische, unbekannte oder stark kommentierte Buecher brauchen meist mehr Zeit.' },
|
|
12
|
+
{ question: 'Garantiert der Plan, dass ich das Buch verstehe?', answer: 'Nein. Er macht aus deinem Tempo und deinem Zeitplan eine Kalender-Schaetzung. Lesefluss und Verstaendnis sind nicht dasselbe, also lies bei anspruchsvollen Stellen langsamer.' },
|
|
13
|
+
{ question: 'Was bedeutet der Aufholpuffer?', answer: 'Er zeigt die Sitzungen, die nach dem Einplanen der geschaetzten Lesezeit bis zur Deadline uebrig bleiben. Ein negativer Wert bedeutet, dass die Kapazitaet des Rhythmus nicht ausreicht.' },
|
|
14
|
+
{ question: 'Warum kann ich nach Seiten oder Woertern planen?', answer: 'Seiten sind praktisch fuer gedruckte Buecher, Woerter besser vergleichbar bei digitalen Texten oder Aufgaben. Waehle die Einheit, die du konsequent verfolgen kannst.' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const howTo = [
|
|
18
|
+
{ name: 'Einheit waehlen', text: 'Zaehle das Buch nach Seiten oder Woertern und trage den Gesamtumfang ein.' },
|
|
19
|
+
{ name: 'Eigenes Tempo messen', text: 'Verwende ein persoenlich gemessenes Tempo statt eines allgemeinen Versprechens zum Schnelllesen.' },
|
|
20
|
+
{ name: 'Wochenrhythmus festlegen', text: 'Setze Minuten pro Sitzung, Sitzungen pro Lesetag und Lesetage pro Woche.' },
|
|
21
|
+
{ name: 'Deadline pruefen', text: 'Sieh dir Ende, Tagestempo und Meilensteine an und reserviere den Puffer fuer echte Unterbrechungen.' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const seo: ToolLocaleContent<BookReadingUI>['seo'] = [
|
|
25
|
+
{ type: 'title', text: 'Lesetempo und Buchdeadline planen', level: 2 },
|
|
26
|
+
{ type: 'paragraph', html: 'Verwandle ein Seiten- oder Woerterziel in einen realistischen Leseplan. Trage Umfang, gemessenes Tempo, Sitzungsdauer, Haeufigkeit und dein Wunschdatum ein.' },
|
|
27
|
+
{ type: 'title', text: 'Mit dem eigenen Lesetempo planen', level: 2 },
|
|
28
|
+
{ type: 'paragraph', html: 'Eine persoenliche Zeitprobe ist nuetzlicher als ein allgemeines Versprechen zum Schnelllesen. Miss einen passenden Abschnitt und senke das Tempo bei dichtem, unbekanntem oder gruendlich zu bearbeitendem Text.' },
|
|
29
|
+
{ type: 'list', items: ['Waehle Seiten fuer ein gedrucktes Buch oder Woerter fuer digitale Texte.', 'Miss einen passenden Abschnitt und verwende ein Tempo, das du mit ausreichendem Verstaendnis halten kannst.', 'Lege Sitzungsdauer, Sitzungen pro Lesetag und Lesetage pro Woche fest.', 'Nutze den Aufholpuffer fuer ausgefallene Tage statt jede Sitzung als Pflicht zu behandeln.', 'Vergleiche den Plan nach den ersten Sitzungen mit deinem tatsaechlichen Fortschritt.'] },
|
|
30
|
+
{ type: 'title', text: 'Die Deadline-Karte lesen', level: 2 },
|
|
31
|
+
{ type: 'paragraph', html: 'Die benoetigte Zeit ist der Aufwand bei deinem gewaehlten Tempo. Benoetigte Lesetage uebersetzen ihn in Sitzungen. Verfuegbare Lesetage schaetzen die Kapazitaet zwischen Start und Deadline anhand deiner Lesetage pro Woche.' },
|
|
32
|
+
{ type: 'table', headers: ['Ausgabe', 'Bedeutung'], rows: [['Tempo bis zur Deadline', 'Umfang fuer jeden verfuegbaren Lesetag'], ['Ziel pro Sitzung', 'Umfang fuer eine geplante Sitzung'], ['Aufholpuffer', 'Uebrige Sitzungen nach dem Einpassen des Aufwands'], ['Voraussichtliches Ende', 'Enddatum bei konstantem Wochenrhythmus']] },
|
|
33
|
+
{ type: 'tip', title: 'Ein Kalender ist kein Verstaendnis', html: 'Der Plan misst weder Verstaendnis noch Schwierigkeit, Ermuedung, Wiederholungen oder Unterbrechungen. Nutze ihn als erste Zusage und aktualisiere ihn mit deinem echten Tempo.' },
|
|
34
|
+
{ type: 'title', text: 'Wenn der Plan knapp oder zu spaet ist', level: 2 },
|
|
35
|
+
{ type: 'paragraph', html: 'Luft im Plan bedeutet Reserve. Ein knapp passender Plan braucht mindestens eine flexible Aufholsitzung. Liegt das Ende nach der Deadline, verlaengere den Zeitraum, fuege Sitzungen hinzu, erhoehe die Lesetage oder reduziere den Umfang.' },
|
|
36
|
+
{ type: 'tip', title: 'Vor dem Optimieren messen', html: 'Vergleiche nach zwei oder drei Sitzungen dein geplantes und dein echtes Ergebnis. Wenn der Unterschied bleibt, aendere das Tempo statt den Kalender gegen ein zu optimistisches Ziel zu zwingen.' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const schemas: [WithContext<SoftwareApplication>, WithContext<FAQPage>, WithContext<HowTo>] = [
|
|
40
|
+
{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'Lesetempo und Buchdeadline planen', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' } },
|
|
41
|
+
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
|
|
42
|
+
{ '@context': 'https://schema.org', '@type': 'HowTo', name: 'Eine Lesefrist planen', step: howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export const content: ToolLocaleContent<BookReadingUI> = { slug: 'buch-lesezeit-und-frist-planer', title: 'Lesetempo und Buchdeadline planen', description: 'Plane Lesetempo, Sitzungsrhythmus und Enddatum für dein Seiten- oder Wörterziel. Berücksichtige die Schwierigkeit, damit dein Kalender zu deinem echten Lesealltag passt.', ui, seo, faq, bibliography, howTo, schemas: schemas as unknown as Record<string, unknown>[] };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { BookReadingUI } from '../ui';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
|
|
5
|
+
|
|
6
|
+
const ui: BookReadingUI = {
|
|
7
|
+
setupLabel: 'Build your reading plan', modeLabel: 'Count by', pagesModeLabel: 'Pages', wordsModeLabel: 'Words', amountLabel: 'Total to read', speedLabel: 'Your reading speed', difficultyLabel: 'Text difficulty', difficultyLightLabel: 'Light · 1.2×', difficultyStandardLabel: 'Standard · 1.0×', difficultyDenseLabel: 'Dense · 0.8×', wordsPerPageLabel: 'Estimated words per page', wordsPerPageHint: 'Typical range: 100 to 500', pagesPerHourLabel: 'pages per hour', wordsPerMinuteLabel: 'words per minute', startDateLabel: 'Start date', deadlineLabel: 'Deadline', daysPerWeekLabel: 'Reading days per week', sessionsPerDayLabel: 'Sessions per reading day', sessionMinutesLabel: 'Minutes per session', minutesRangeLabel: '5 to 240 min', hourShortLabel: 'h', minuteShortLabel: 'min', bufferShortLabel: 'short', bufferExtraLabel: 'extra', speedTestLabel: 'Measure your reading speed', speedTestHint: 'Use a short representative passage', speedTestPromptLabel: 'Passage to read', speedTestPlaceholder: 'Paste or type a sentence or short passage here...', speedTestStartLabel: 'Start timer', speedTestFinishLabel: 'I finished', speedTestElapsedLabel: 'Elapsed', speedTestRateLabel: 'Estimated pace', speedTestSecondsLabel: 'sec', speedTestWpmLabel: 'WPM', speedTestUseLabel: 'Use this WPM in Words mode', speedTestIdle: 'Enter a passage, then start when you are ready.', speedTestRunning: 'Timer running. Read at your natural pace.', speedTestFinished: 'Test complete. Treat this as a first estimate, not a benchmark.', speedTestNeedsText: 'Add a passage before starting the timer.', speedTestNoWords: 'No words measured', presetLabel: 'Quick rhythm', presetSteadyLabel: 'Steady week', presetWeekendLabel: 'Weekend focus', presetAcademicLabel: 'Academic sprint', planLabel: 'Your reading map', timeNeededLabel: 'Time needed', readingDaysLabel: 'Reading days needed', capacityLabel: 'Reading days available', dailyPaceLabel: 'Pace by deadline', estimatedWordsLabel: 'Estimated words', wordsShortLabel: 'words', sessionTargetLabel: 'Target per session', catchUpLabel: 'Catch-up buffer', finishDateLabel: 'Projected finish', milestoneLabel: 'Milestones', timelineLabel: 'Reading milestone timeline', calendarHint: 'Milestones are planning markers. Move the deadline or adjust your rhythm when real life interrupts.', exportCalendarLabel: 'Download milestones (.ics)', exportCalendarTitle: 'Reading milestones', milestoneStart: 'Start', milestoneQuarter: '25%', milestoneHalf: 'Half', milestoneThreeQuarter: '75%', milestoneFinish: 'Finish', statusOnTrack: 'Room to breathe', statusTight: 'A close fit', statusLate: 'Past the deadline', statusInvalid: 'Check the dates', statusOnTrackDetail: 'Your available sessions leave a useful buffer for missed days or a slower chapter.', statusTightDetail: 'The plan fits, but there is little spare capacity. Keep one session flexible for catch-up.', statusLateDetail: 'The current rhythm cannot fit before the deadline. Add sessions, extend the date, or reduce the target.', statusInvalidDetail: 'Choose a valid start date and a deadline on or after it.', sourceLabel: 'This is a pacing estimate, not a promise of comprehension or a fixed reading speed. Measure your own rate on a representative passage.',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const faq = [
|
|
11
|
+
{ question: 'How should I choose my reading speed?', answer: 'Time yourself on a representative passage, count its pages or words, and use that observed rate. Dense, technical, unfamiliar or heavily annotated books usually take longer.' },
|
|
12
|
+
{ question: 'Does the planner guarantee that I will understand the book?', answer: 'No. It turns your own pace and schedule into a calendar estimate. Reading fluency and comprehension are related but not interchangeable, so slow down when the text requires close study.' },
|
|
13
|
+
{ question: 'What does the catch-up buffer mean?', answer: 'It is the number of spare sessions left after the estimated reading time is placed inside your deadline. A negative value means the current rhythm is short of the required capacity.' },
|
|
14
|
+
{ question: 'Why can I plan by pages or words?', answer: 'Pages are convenient for a printed book, while words can be more comparable across digital layouts or assigned passages. Choose the measure you can track consistently.' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const howTo = [
|
|
18
|
+
{ name: 'Choose the reading measure', text: 'Count the book by pages or words, then enter the total amount.' },
|
|
19
|
+
{ name: 'Enter your observed speed', text: 'Use a measured personal pace rather than a generic speed-reading claim.' },
|
|
20
|
+
{ name: 'Shape the weekly rhythm', text: 'Set minutes per session, sessions per reading day and reading days per week.' },
|
|
21
|
+
{ name: 'Set the deadline and inspect the map', text: 'Review the projected finish, pace by deadline and milestone dates, then reserve the buffer for real interruptions.' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const seo: ToolLocaleContent<BookReadingUI>['seo'] = [
|
|
25
|
+
{ type: 'title', text: 'Plan Your Reading Pace and Book Deadline', level: 2 },
|
|
26
|
+
{ type: 'paragraph', html: 'Turn a page or word target into a realistic reading calendar. Enter the amount you want to finish, your measured reading speed, the length and frequency of sessions, and the date you want to be done.' },
|
|
27
|
+
{ type: 'title', text: 'Use Your Own Reading Rate', level: 2 },
|
|
28
|
+
{ type: 'paragraph', html: 'A personal timed sample is more useful than a universal reading-speed promise. Test a representative passage, keep the unit consistent, and lower the rate when the book is dense, unfamiliar or meant for close study.' },
|
|
29
|
+
{ type: 'list', items: ['Choose Pages for a printed book or Words for a digital or assigned passage.', 'Time a representative sample and enter the rate you can sustain with the understanding you need.', 'Set your real session length, sessions per reading day and reading days per week.', 'Use the catch-up buffer for missed days instead of treating every planned session as mandatory.', 'Recheck the plan after the first few sessions and replace the estimate with your observed pace.'] },
|
|
30
|
+
{ type: 'title', text: 'Read the Deadline Map', level: 2 },
|
|
31
|
+
{ type: 'paragraph', html: 'Time needed is the estimated effort at your chosen rate. Reading days needed turns that effort into sessions at your session length. Available reading days estimates the capacity between your start and deadline from the number of reading days per week, without pretending to know which weekdays you will actually use.' },
|
|
32
|
+
{ type: 'table', headers: ['Output', 'What it tells you'], rows: [['Pace by deadline', 'The amount to cover on each available reading day'], ['Target per session', 'The amount to aim for in one planned session'], ['Catch-up buffer', 'Spare sessions after the estimated work fits'], ['Projected finish', 'The finish date if your weekly rhythm stays constant']] },
|
|
33
|
+
{ type: 'tip', title: 'A calendar is not comprehension', html: 'The planner does not measure understanding, difficulty, fatigue, rereading or interruptions. Use it to make a first commitment, then update it with your real pace and leave room for notes, review and difficult chapters.' },
|
|
34
|
+
{ type: 'title', text: 'When the Plan Is Tight or Late', level: 2 },
|
|
35
|
+
{ type: 'paragraph', html: 'Room to breathe means the estimated sessions fit with spare capacity. A close fit has a small buffer and deserves one flexible catch-up slot. Past the deadline means the configured schedule cannot hold the target, so change one of the levers: extend the deadline, add sessions, add reading days or reduce the amount.' },
|
|
36
|
+
{ type: 'tip', title: 'Measure before you optimise', html: 'After two or three sessions, compare the planned pace with what you actually finished. If the difference persists, edit the speed rather than forcing the schedule to match an optimistic estimate.' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const schemas: [WithContext<SoftwareApplication>, WithContext<FAQPage>, WithContext<HowTo>] = [
|
|
40
|
+
{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'Book Reading Time Deadline Planner', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } },
|
|
41
|
+
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
|
|
42
|
+
{ '@context': 'https://schema.org', '@type': 'HowTo', name: 'Plan a reading deadline', step: howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export const content: ToolLocaleContent<BookReadingUI> = {
|
|
46
|
+
slug: 'book-reading-time-deadline-planner',
|
|
47
|
+
title: 'Book Reading Time Deadline Planner',
|
|
48
|
+
description: 'Plan a realistic reading pace, session rhythm and finish date from your page or word target.',
|
|
49
|
+
ui, seo, faq, bibliography, howTo,
|
|
50
|
+
schemas: schemas as unknown as Record<string, unknown>[],
|
|
51
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { BookReadingUI } from '../ui';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
|
|
5
|
+
|
|
6
|
+
const ui: BookReadingUI = {
|
|
7
|
+
setupLabel: 'Crea tu plan de lectura', modeLabel: 'Contar por', pagesModeLabel: 'Páginas', wordsModeLabel: 'Palabras', amountLabel: 'Total por leer', speedLabel: 'Tu velocidad de lectura', difficultyLabel: 'Dificultad del texto', difficultyLightLabel: 'Ligero · 1.2x', difficultyStandardLabel: 'Estándar · 1.0x', difficultyDenseLabel: 'Denso · 0.8x', wordsPerPageLabel: 'Palabras estimadas por página', wordsPerPageHint: 'Rango habitual: 100 a 500', pagesPerHourLabel: 'páginas por hora', wordsPerMinuteLabel: 'palabras por minuto', startDateLabel: 'Fecha de inicio', deadlineLabel: 'Fecha límite', daysPerWeekLabel: 'Días de lectura por semana', sessionsPerDayLabel: 'Sesiones por día de lectura', sessionMinutesLabel: 'Minutos por sesión', minutesRangeLabel: 'De 5 a 240 min', hourShortLabel: 'h', minuteShortLabel: 'min', bufferShortLabel: 'cortas', bufferExtraLabel: 'extra', speedTestLabel: 'Mide tu velocidad de lectura', speedTestHint: 'Usa un fragmento breve y representativo', speedTestPromptLabel: 'Texto que vas a leer', speedTestPlaceholder: 'Pega o escribe aquí una frase o un fragmento breve...', speedTestStartLabel: 'Iniciar temporizador', speedTestFinishLabel: 'He terminado', speedTestElapsedLabel: 'Tiempo', speedTestRateLabel: 'Ritmo estimado', speedTestSecondsLabel: 's', speedTestWpmLabel: 'PPM', speedTestUseLabel: 'Usar estas PPM en modo Palabras', speedTestIdle: 'Escribe un texto y empieza cuando estés preparado.', speedTestRunning: 'Temporizador activo. Lee a tu ritmo natural.', speedTestFinished: 'Prueba terminada. Es una primera estimación, no una referencia fija.', speedTestNeedsText: 'Añade un texto antes de iniciar el temporizador.', speedTestNoWords: 'No se han medido palabras', presetLabel: 'Ritmo rápido', presetSteadyLabel: 'Semana estable', presetWeekendLabel: 'Fin de semana', presetAcademicLabel: 'Ritmo académico', planLabel: 'Tu mapa de lectura', timeNeededLabel: 'Tiempo necesario', readingDaysLabel: 'Días de lectura necesarios', capacityLabel: 'Días de lectura disponibles', dailyPaceLabel: 'Ritmo hasta la fecha límite', estimatedWordsLabel: 'Palabras estimadas', wordsShortLabel: 'palabras', sessionTargetLabel: 'Objetivo por sesión', catchUpLabel: 'Margen para recuperar', finishDateLabel: 'Fin previsto', milestoneLabel: 'Hitos', timelineLabel: 'Línea temporal de lectura', calendarHint: 'Los hitos son referencias de planificación. Mueve la fecha límite o ajusta tu ritmo cuando la vida real interrumpa el plan.', exportCalendarLabel: 'Descargar hitos (.ics)', exportCalendarTitle: 'Hitos de lectura', milestoneStart: 'Inicio', milestoneQuarter: '25 %', milestoneHalf: 'Mitad', milestoneThreeQuarter: '75 %', milestoneFinish: 'Fin', statusOnTrack: 'Hay margen', statusTight: 'Ajuste justo', statusLate: 'Después de la fecha límite', statusInvalid: 'Revisa las fechas', statusOnTrackDetail: 'Tus sesiones disponibles dejan margen para días perdidos o capítulos más lentos.', statusTightDetail: 'El plan encaja, pero tiene poca reserva. Deja una sesión flexible para recuperar.', statusLateDetail: 'El ritmo actual no llega a la fecha límite. Añade sesiones, amplía la fecha o reduce el objetivo.', statusInvalidDetail: 'Elige una fecha de inicio válida y una fecha límite igual o posterior.', sourceLabel: 'Esto es una estimación de ritmo, no una promesa de comprensión ni una velocidad de lectura fija. Mide tu propio ritmo con un texto representativo.',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const faq = [
|
|
11
|
+
{ question: '¿Cómo elijo mi velocidad de lectura?', answer: 'Cronometra un fragmento representativo, cuenta sus páginas o palabras y usa el ritmo observado. Los libros densos, técnicos, desconocidos o muy anotados suelen requerir más tiempo.' },
|
|
12
|
+
{ question: '¿El plan garantiza que entenderé el libro?', answer: 'No. Convierte tu ritmo y tu calendario en una estimación. Fluidez y comprensión no son lo mismo, así que reduce la velocidad cuando el texto requiera estudio detallado.' },
|
|
13
|
+
{ question: '¿Qué significa el margen para recuperar?', answer: 'Son las sesiones que quedan después de encajar el tiempo estimado dentro de la fecha límite. Un valor negativo indica que el ritmo configurado no tiene capacidad suficiente.' },
|
|
14
|
+
{ question: '¿Por qué puedo planificar por páginas o palabras?', answer: 'Las páginas son prácticas para un libro impreso y las palabras permiten comparar mejor textos digitales o lecturas asignadas. Elige la unidad que puedas seguir de forma constante.' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const howTo = [
|
|
18
|
+
{ name: 'Elige la unidad de lectura', text: 'Cuenta el libro por páginas o palabras y escribe el total.' },
|
|
19
|
+
{ name: 'Mide tu ritmo real', text: 'Usa una velocidad personal medida, no una promesa genérica de lectura rápida.' },
|
|
20
|
+
{ name: 'Define tu ritmo semanal', text: 'Configura minutos por sesión, sesiones por día de lectura y días de lectura por semana.' },
|
|
21
|
+
{ name: 'Revisa la fecha límite', text: 'Consulta el fin previsto, el ritmo diario y los hitos; reserva el margen para interrupciones reales.' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const seo: ToolLocaleContent<BookReadingUI>['seo'] = [
|
|
25
|
+
{ type: 'title', text: 'Planificador de ritmo de lectura y fecha límite del libro', level: 2 },
|
|
26
|
+
{ type: 'paragraph', html: 'Convierte un objetivo de páginas o palabras en un calendario de lectura realista. Introduce el volumen, tu velocidad medida, la duración y frecuencia de las sesiones y la fecha en la que quieres terminar.' },
|
|
27
|
+
{ type: 'title', text: 'Usa tu propio ritmo de lectura', level: 2 },
|
|
28
|
+
{ type: 'paragraph', html: 'Una prueba cronometrada personal es más útil que una promesa universal de lectura rápida. Mide un fragmento representativo y baja el ritmo cuando el libro sea denso, desconocido o requiera una lectura atenta.' },
|
|
29
|
+
{ type: 'list', items: ['Elige Páginas para un libro impreso o Palabras para un texto digital o asignado.', 'Cronometra un fragmento representativo y usa el ritmo que puedas mantener con la comprensión que necesitas.', 'Define la duración de las sesiones, las sesiones por día y los días de lectura por semana.', 'Usa el margen para recuperar días perdidos en lugar de tratar cada sesión como obligatoria.', 'Compara el plan con lo que realmente avanzas después de las primeras sesiones.'] },
|
|
30
|
+
{ type: 'title', text: 'Interpreta el mapa de la fecha límite', level: 2 },
|
|
31
|
+
{ type: 'paragraph', html: 'El tiempo necesario es el esfuerzo estimado a tu ritmo. Los días necesarios lo convierten en sesiones. Los días disponibles estiman la capacidad entre el inicio y la fecha límite según tus días de lectura semanales.' },
|
|
32
|
+
{ type: 'table', headers: ['Resultado', 'Qué indica'], rows: [['Ritmo hasta la fecha límite', 'Cantidad para cada día de lectura disponible'], ['Objetivo por sesión', 'Cantidad para una sesión planificada'], ['Margen para recuperar', 'Sesiones sobrantes después de encajar el esfuerzo'], ['Fin previsto', 'Fecha de finalización si mantienes el ritmo semanal']] },
|
|
33
|
+
{ type: 'tip', title: 'Un calendario no mide la comprensión', html: 'El plan no mide comprensión, dificultad, cansancio, relecturas ni interrupciones. Úsalo como primer compromiso y actualízalo con tu ritmo real, dejando espacio para capítulos difíciles.' },
|
|
34
|
+
{ type: 'title', text: 'Cuando el plan queda justo o llega tarde', level: 2 },
|
|
35
|
+
{ type: 'paragraph', html: 'Hay margen cuando las sesiones estimadas caben con reserva. Un ajuste justo merece al menos una sesión flexible. Si el plan supera la fecha límite, amplía el plazo, añade sesiones, aumenta los días de lectura o reduce el objetivo.' },
|
|
36
|
+
{ type: 'tip', title: 'Mide antes de optimizar', html: 'Después de dos o tres sesiones, compara el avance previsto con el real. Si la diferencia continúa, cambia la velocidad en vez de forzar el calendario con una estimación demasiado optimista.' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const schemas: [WithContext<SoftwareApplication>, WithContext<FAQPage>, WithContext<HowTo>] = [
|
|
40
|
+
{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'Planificador de ritmo de lectura y fecha límite del libro', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' } },
|
|
41
|
+
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
|
|
42
|
+
{ '@context': 'https://schema.org', '@type': 'HowTo', name: 'Planificar una fecha de lectura', step: howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export const content: ToolLocaleContent<BookReadingUI> = { slug: 'planificador-tiempo-lectura-y-fecha-limite-libro', title: 'Planificador de ritmo de lectura y fecha límite del libro', description: 'Planifica un ritmo de lectura realista, un horario de sesiones y una fecha de finalización a partir de tus páginas o palabras.', ui, seo, faq, bibliography, howTo, schemas: schemas as unknown as Record<string, unknown>[] };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { BookReadingUI } from '../ui';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
|
|
5
|
+
|
|
6
|
+
const ui: BookReadingUI = {
|
|
7
|
+
setupLabel: 'Construire votre plan de lecture', modeLabel: 'Compter en', pagesModeLabel: 'Pages', wordsModeLabel: 'Mots', amountLabel: 'Total à lire', speedLabel: 'Votre vitesse de lecture', difficultyLabel: 'Difficulté du texte', difficultyLightLabel: 'Léger · 1.2x', difficultyStandardLabel: 'Standard · 1.0x', difficultyDenseLabel: 'Dense · 0.8x', wordsPerPageLabel: 'Mots estimés par page', wordsPerPageHint: 'Fourchette habituelle: 100 à 500', pagesPerHourLabel: 'pages par heure', wordsPerMinuteLabel: 'mots par minute', startDateLabel: 'Date de début', deadlineLabel: 'Date limite', daysPerWeekLabel: 'Jours de lecture par semaine', sessionsPerDayLabel: 'Sessions par jour de lecture', sessionMinutesLabel: 'Minutes par session', minutesRangeLabel: 'De 5 à 240 min', hourShortLabel: 'h', minuteShortLabel: 'min', bufferShortLabel: 'manquantes', bufferExtraLabel: 'en plus', speedTestLabel: 'Mesurer votre vitesse de lecture', speedTestHint: 'Utilisez un passage court et représentatif', speedTestPromptLabel: 'Passage à lire', speedTestPlaceholder: 'Collez ou saisissez une phrase ou un court passage ici...', speedTestStartLabel: 'Démarrer le minuteur', speedTestFinishLabel: "J'ai terminé", speedTestElapsedLabel: 'Temps écoulé', speedTestRateLabel: 'Rythme estimé', speedTestSecondsLabel: 's', speedTestWpmLabel: 'MPM', speedTestUseLabel: 'Utiliser ces MPM en mode Mots', speedTestIdle: 'Saisissez un passage puis démarrez quand vous êtes prêt.', speedTestRunning: 'Minuteur en cours. Lisez à votre rythme naturel.', speedTestFinished: "Test terminé. Il s'agit d'une première estimation, pas d'une norme.", speedTestNeedsText: 'Ajoutez un passage avant de démarrer le minuteur.', speedTestNoWords: 'Aucun mot mesuré', presetLabel: 'Rythme rapide', presetSteadyLabel: 'Semaine régulière', presetWeekendLabel: 'Focus week-end', presetAcademicLabel: 'Rythme intensif', planLabel: 'Votre carte de lecture', timeNeededLabel: 'Temps nécessaire', readingDaysLabel: 'Jours de lecture nécessaires', capacityLabel: 'Jours de lecture disponibles', dailyPaceLabel: "Rythme jusqu'à la date limite", estimatedWordsLabel: 'Mots estimés', wordsShortLabel: 'mots', sessionTargetLabel: 'Objectif par session', catchUpLabel: 'Marge de rattrapage', finishDateLabel: 'Fin prévue', milestoneLabel: 'Étapes', timelineLabel: 'Calendrier des étapes de lecture', calendarHint: 'Les étapes sont des repères de planification. Déplacez la date limite ou adaptez votre rythme quand la vie réelle interrompt le programme.', exportCalendarLabel: 'Télécharger les étapes (.ics)', exportCalendarTitle: 'Étapes de lecture', milestoneStart: 'Début', milestoneQuarter: '25 %', milestoneHalf: 'Moitié', milestoneThreeQuarter: '75 %', milestoneFinish: 'Fin', statusOnTrack: 'Marge disponible', statusTight: 'Ajusté', statusLate: 'Après la date limite', statusInvalid: 'Vérifier les dates', statusOnTrackDetail: 'Vos sessions disponibles laissent une marge pour les jours manqués ou un chapitre plus lent.', statusTightDetail: 'Le plan tient, mais la réserve est faible. Gardez une session flexible pour rattraper le retard.', statusLateDetail: "Le rythme actuel ne permet pas de tenir la date limite. Ajoutez des sessions, prolongez la période ou réduisez l'objectif.", statusInvalidDetail: 'Choisissez une date de début valide et une date limite égale ou postérieure.', sourceLabel: "Il s'agit d'une estimation de rythme, pas d'une promesse de compréhension ni d'une vitesse fixe. Mesurez votre propre rythme sur un passage représentatif.",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const faq = [
|
|
11
|
+
{ question: 'Comment choisir ma vitesse de lecture ?', answer: 'Chronometrez un passage representatif, comptez ses pages ou ses mots et utilisez le rythme observe. Les livres denses, techniques, inconnus ou tres annotees demandent souvent plus de temps.' },
|
|
12
|
+
{ question: 'Le plan garantit-il que je comprendrai le livre ?', answer: 'Non. Il transforme votre rythme et votre calendrier en estimation. La fluidite et la comprehension ne sont pas equivalentes, alors ralentissez quand le texte exige une lecture attentive.' },
|
|
13
|
+
{ question: 'Que signifie la marge de rattrapage ?', answer: 'Elle indique les sessions restantes apres avoir place le temps estime avant la date limite. Une valeur negative signifie que le rythme choisi n offre pas assez de capacite.' },
|
|
14
|
+
{ question: 'Pourquoi planifier en pages ou en mots ?', answer: 'Les pages sont pratiques pour un livre imprime, tandis que les mots sont plus comparables dans les textes numeriques ou imposes. Choisissez l unite que vous pouvez suivre regulierement.' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const howTo = [
|
|
18
|
+
{ name: 'Choisir l unite de lecture', text: 'Comptez le livre en pages ou en mots, puis saisissez le total.' },
|
|
19
|
+
{ name: 'Mesurer votre rythme', text: 'Utilisez un rythme personnel mesure plutot qu une promesse generale de lecture rapide.' },
|
|
20
|
+
{ name: 'Definir le rythme hebdomadaire', text: 'Reglez les minutes par session, les sessions par jour et les jours de lecture par semaine.' },
|
|
21
|
+
{ name: 'Examiner la date limite', text: 'Consultez la fin prevue, le rythme quotidien et les etapes, puis gardez la marge pour les interruptions reelles.' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const seo: ToolLocaleContent<BookReadingUI>['seo'] = [
|
|
25
|
+
{ type: 'title', text: 'Planifier le rythme de lecture et la date limite d un livre', level: 2 },
|
|
26
|
+
{ type: 'paragraph', html: 'Transformez un objectif de pages ou de mots en calendrier de lecture realiste. Saisissez le volume, votre vitesse mesuree, la duree et la frequence des sessions ainsi que la date souhaitee.' },
|
|
27
|
+
{ type: 'title', text: 'Utiliser votre propre rythme de lecture', level: 2 },
|
|
28
|
+
{ type: 'paragraph', html: 'Un test chronometre personnel est plus utile qu une promesse universelle de lecture rapide. Testez un passage representatif et ralentissez si le livre est dense, inconnu ou destine a une etude attentive.' },
|
|
29
|
+
{ type: 'list', items: ['Choisissez Pages pour un livre imprime ou Mots pour un texte numerique ou impose.', 'Chronometrez un passage representatif et gardez un rythme compatible avec la comprehension souhaitee.', 'Definissez la duree des sessions, les sessions par jour et les jours de lecture par semaine.', 'Utilisez la marge pour les jours manques au lieu de rendre chaque session obligatoire.', 'Comparez le plan avec votre progression reelle apres les premieres sessions.'] },
|
|
30
|
+
{ type: 'title', text: 'Lire la carte de la date limite', level: 2 },
|
|
31
|
+
{ type: 'paragraph', html: 'Le temps necessaire represente l effort estime a votre rythme. Les jours necessaires le convertissent en sessions. Les jours disponibles estiment la capacite entre le debut et la date limite selon vos jours de lecture hebdomadaires.' },
|
|
32
|
+
{ type: 'table', headers: ['Resultat', 'Ce qu il indique'], rows: [['Rythme jusqu a la date limite', 'Quantite a couvrir chaque jour disponible'], ['Objectif par session', 'Quantite a viser pendant une session'], ['Marge de rattrapage', 'Sessions restantes apres l estimation'], ['Fin prevue', 'Date de fin si le rythme hebdomadaire reste constant']] },
|
|
33
|
+
{ type: 'tip', title: 'Un calendrier ne mesure pas la comprehension', html: 'Le plan ne mesure ni la comprehension, ni la difficulte, ni la fatigue, ni les relectures, ni les interruptions. Utilisez-le comme premier engagement et mettez-le a jour avec votre rythme reel.' },
|
|
34
|
+
{ type: 'title', text: 'Quand le plan est serre ou en retard', level: 2 },
|
|
35
|
+
{ type: 'paragraph', html: 'Une marge disponible signifie que les sessions estimees tiennent avec une reserve. Un plan serre merite au moins une session flexible. Si la fin depasse la date limite, prolongez la periode, ajoutez des sessions ou reduisez l objectif.' },
|
|
36
|
+
{ type: 'tip', title: 'Mesurer avant d optimiser', html: 'Apres deux ou trois sessions, comparez la progression prevue et la progression reelle. Si l ecart persiste, modifiez la vitesse plutot que de forcer un calendrier trop optimiste.' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const schemas: [WithContext<SoftwareApplication>, WithContext<FAQPage>, WithContext<HowTo>] = [
|
|
40
|
+
{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'Planifier le rythme de lecture et la date limite d un livre', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' } },
|
|
41
|
+
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
|
|
42
|
+
{ '@context': 'https://schema.org', '@type': 'HowTo', name: 'Planifier une date de lecture', step: howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export const content: ToolLocaleContent<BookReadingUI> = { slug: 'planificateur-temps-lecture-et-echeance-livre', title: 'Planifier le rythme de lecture et la date limite d un livre', description: 'Planifiez un rythme de lecture realiste, un rythme de sessions et une date de fin a partir de votre objectif de pages ou de mots.', ui, seo, faq, bibliography, howTo, schemas: schemas as unknown as Record<string, unknown>[] };
|