moracarta 2.1.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/LICENSE +21 -0
- package/README.md +84 -0
- package/index.html +31 -0
- package/package.json +35 -0
- package/public/assets/images/corner.webp +0 -0
- package/public/assets/images/custom-text-demo1.png +0 -0
- package/public/assets/images/custom-text-demo2.png +0 -0
- package/public/assets/images/demo-animation.gif +0 -0
- package/public/assets/images/demo-main-page.png +0 -0
- package/public/assets/images/googleDocsTabs.webp +0 -0
- package/public/assets/images/setup-demo.png +0 -0
- package/public/robots.txt +2 -0
- package/src/commands/add.js +170 -0
- package/src/commands/cliConfig.js +68 -0
- package/src/commands/deploy.js +41 -0
- package/src/commands/remove.js +198 -0
- package/src/commands/setup.js +257 -0
- package/src/config/fonts.js +7 -0
- package/src/data/letters.example.js +28 -0
- package/src/data/lettersLoader.js +7 -0
- package/src/i18n/de.js +8 -0
- package/src/i18n/en.js +8 -0
- package/src/i18n/es.js +8 -0
- package/src/i18n/fr.js +8 -0
- package/src/i18n/it.js +8 -0
- package/src/i18n/ja.js +8 -0
- package/src/i18n/ko.js +8 -0
- package/src/i18n/nl.js +0 -0
- package/src/i18n/pr.js +0 -0
- package/src/i18n/pt-BR.js +8 -0
- package/src/i18n/ru.js +8 -0
- package/src/i18n/tl.js +0 -0
- package/src/i18n/zh.js +8 -0
- package/src/scripts/data.variable.js +24 -0
- package/src/scripts/i18n.js +67 -0
- package/src/scripts/letters.script.js +257 -0
- package/src/scripts/main.script.js +101 -0
- package/src/services/googleDocs.js +76 -0
- package/src/styles/letters.css +405 -0
- package/src/styles/main.css +338 -0
- package/src/views/letters.view.html +83 -0
- package/vite.config.mjs +16 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { getTranslation } from './i18n.js';
|
|
2
|
+
import { letters } from '../data/lettersLoader.js';
|
|
3
|
+
|
|
4
|
+
console.log('letters.script loaded');
|
|
5
|
+
|
|
6
|
+
const params = new URLSearchParams(window.location.search);
|
|
7
|
+
const requestedId = parseInt(params.get('id'), 10);
|
|
8
|
+
const letter = letters.find((entry) => entry.id === requestedId);
|
|
9
|
+
|
|
10
|
+
// A set of base color families used to generate a unique yet consistent
|
|
11
|
+
// envelope palette for each letter based on its id, title, and date.
|
|
12
|
+
const paletteFamilies = [
|
|
13
|
+
{ hue: 18, sat: 54, light: 88, sealHue: 345, sealSat: 76, sealLight: 42 },
|
|
14
|
+
{ hue: 26, sat: 48, light: 86, sealHue: 12, sealSat: 72, sealLight: 38 },
|
|
15
|
+
{ hue: 4, sat: 50, light: 84, sealHue: 5, sealSat: 78, sealLight: 34 },
|
|
16
|
+
{ hue: 38, sat: 42, light: 90, sealHue: 24, sealSat: 66, sealLight: 36 },
|
|
17
|
+
{ hue: 8, sat: 58, light: 86, sealHue: 6, sealSat: 82, sealLight: 30 },
|
|
18
|
+
{ hue: 12, sat: 50, light: 88, sealHue: 11, sealSat: 74, sealLight: 33 },
|
|
19
|
+
{ hue: 22, sat: 46, light: 92, sealHue: 20, sealSat: 68, sealLight: 40 },
|
|
20
|
+
{ hue: 16, sat: 60, light: 84, sealHue: 5, sealSat: 86, sealLight: 32 },
|
|
21
|
+
{ hue: 28, sat: 52, light: 90, sealHue: 15, sealSat: 70, sealLight: 38 },
|
|
22
|
+
{ hue: 14, sat: 44, light: 88, sealHue: 8, sealSat: 80, sealLight: 36 }
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function hashString(value) {
|
|
26
|
+
// Simple deterministic string hash used to select a palette and variant.
|
|
27
|
+
// This keeps each letter appearance stable across page loads.
|
|
28
|
+
let hash = 0;
|
|
29
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
30
|
+
hash = ((hash << 5) - hash) + value.charCodeAt(index);
|
|
31
|
+
hash |= 0;
|
|
32
|
+
}
|
|
33
|
+
return Math.abs(hash);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function clamp(value, min, max) {
|
|
37
|
+
return Math.min(Math.max(value, min), max);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function generatePalette(letterItem) {
|
|
41
|
+
// Build a deterministic palette based on the letter's identity.
|
|
42
|
+
// The base hash selects one of the palette families, while the variant
|
|
43
|
+
// arrays add subtle shifts to keep similar letters visually distinct.
|
|
44
|
+
const baseHash = hashString(`${letterItem.id}|${letterItem.title}|${letterItem.date}`);
|
|
45
|
+
const family = paletteFamilies[baseHash % paletteFamilies.length];
|
|
46
|
+
const variant = Math.floor(baseHash / paletteFamilies.length) % 7;
|
|
47
|
+
const hueShift = [0, 14, -12, 20, -18, 26, -22][variant];
|
|
48
|
+
const satShift = [0, 8, -6, 12, -10, 14, -8][variant];
|
|
49
|
+
const lightShift = [0, -8, 8, -5, 6, -10, 10][variant];
|
|
50
|
+
|
|
51
|
+
const envelope = `hsl(${clamp(family.hue + hueShift, 0, 360)}, ${clamp(family.sat + satShift, 42, 72)}%, ${clamp(family.light + lightShift, 78, 92)}%)`;
|
|
52
|
+
const flap = `hsl(${clamp(family.hue + hueShift + 2, 0, 360)}, ${clamp(family.sat + satShift + 8, 50, 82)}%, ${clamp(family.light - 18 + lightShift, 60, 76)}%)`;
|
|
53
|
+
const shadow = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 12, 30, 58)}%, ${clamp(family.light - 36 + lightShift, 42, 58)}%)`;
|
|
54
|
+
const base = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 10, 30, 58)}%, ${clamp(family.light - 30 + lightShift, 44, 62)}%)`;
|
|
55
|
+
const seal = `hsl(${clamp(family.sealHue + hueShift, 0, 360)}, ${clamp(family.sealSat + satShift, 68, 92)}%, ${clamp(family.sealLight + lightShift * 0.8, 28, 46)}%)`;
|
|
56
|
+
const text = `hsl(${family.hue}, 22%, 18%)`;
|
|
57
|
+
|
|
58
|
+
return { envelope, flap, shadow, base, seal, text };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function applyEnvelopePalette(letterItem) {
|
|
62
|
+
const palette = generatePalette(letterItem);
|
|
63
|
+
document.querySelector('.env-fundo').style.background = palette.envelope;
|
|
64
|
+
|
|
65
|
+
const flapPolygon = document.querySelector('.env-aba svg polygon');
|
|
66
|
+
if (flapPolygon) {
|
|
67
|
+
flapPolygon.setAttribute('fill', palette.flap);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const sidePolygons = document.querySelectorAll('.env-corpo-svg svg polygon');
|
|
71
|
+
if (sidePolygons.length === 3) {
|
|
72
|
+
sidePolygons[0].setAttribute('fill', palette.shadow);
|
|
73
|
+
sidePolygons[1].setAttribute('fill', palette.shadow);
|
|
74
|
+
sidePolygons[2].setAttribute('fill', palette.base);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!letter) {
|
|
79
|
+
document.getElementById('error-message').style.display = 'block';
|
|
80
|
+
document.getElementById('envelope-wrap').style.display = 'none';
|
|
81
|
+
document.getElementById('letter-wrap').style.display = 'none';
|
|
82
|
+
document.getElementById('letter-nav').style.display = 'none';
|
|
83
|
+
} else {
|
|
84
|
+
document.title = letter.title;
|
|
85
|
+
|
|
86
|
+
document.getElementById('letter-date').textContent = letter.date;
|
|
87
|
+
document.getElementById('letter-title').textContent = letter.title;
|
|
88
|
+
document.getElementById('letter-body').innerHTML = letter.content
|
|
89
|
+
.split(/\n\s*\n/)
|
|
90
|
+
.filter(paragraph => paragraph.trim())
|
|
91
|
+
.map(paragraph => `<p>${paragraph.trimEnd()}</p>`)
|
|
92
|
+
.join('');
|
|
93
|
+
|
|
94
|
+
applyEnvelopePalette(letter);
|
|
95
|
+
document.querySelector('.envelope-stage').classList.add('ready');
|
|
96
|
+
|
|
97
|
+
const sortedLetters = letters.slice().sort((first, second) => first.id - second.id);
|
|
98
|
+
const currentIndex = sortedLetters.findIndex((entry) => entry.id === requestedId);
|
|
99
|
+
const previousLetter = currentIndex > 0 ? sortedLetters[currentIndex - 1] : null;
|
|
100
|
+
const nextLetter = currentIndex < sortedLetters.length - 1 ? sortedLetters[currentIndex + 1] : null;
|
|
101
|
+
|
|
102
|
+
document.getElementById('letter-counter').textContent = `${currentIndex + 1} ${getTranslation('OF')} ${sortedLetters.length}`;
|
|
103
|
+
|
|
104
|
+
if (previousLetter) {
|
|
105
|
+
const previousButton = document.getElementById('nav-previous');
|
|
106
|
+
previousButton.href = `./letters.view.html?id=${previousLetter.id}`;
|
|
107
|
+
previousButton.classList.remove('invisible');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (nextLetter) {
|
|
111
|
+
const nextButton = document.getElementById('nav-next');
|
|
112
|
+
nextButton.href = `./letters.view.html?id=${nextLetter.id}`;
|
|
113
|
+
nextButton.classList.remove('invisible');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
startHeartAnimation();
|
|
117
|
+
|
|
118
|
+
// Animate the envelope opening, sealing, and letter reveal in timed stages.
|
|
119
|
+
window.setTimeout(() => {
|
|
120
|
+
document.getElementById('flap').classList.add('open');
|
|
121
|
+
document.getElementById('seal').classList.add('fading');
|
|
122
|
+
}, 400);
|
|
123
|
+
|
|
124
|
+
window.setTimeout(() => {
|
|
125
|
+
document.getElementById('flap').classList.add('behind');
|
|
126
|
+
}, 1050);
|
|
127
|
+
|
|
128
|
+
window.setTimeout(() => {
|
|
129
|
+
document.getElementById('paper').classList.add('leaving');
|
|
130
|
+
}, 900);
|
|
131
|
+
|
|
132
|
+
window.setTimeout(() => {
|
|
133
|
+
document.getElementById('letter-wrap').classList.add('visible');
|
|
134
|
+
document.getElementById('letter-nav').classList.add('visible');
|
|
135
|
+
document.getElementById('envelope-wrap').style.display = 'none';
|
|
136
|
+
|
|
137
|
+
}, 2100);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function startHeartAnimation() {
|
|
141
|
+
// Floating heart particle animation for the letter view background.
|
|
142
|
+
// Particles are born after a delay, float upward, fade at the top, and die
|
|
143
|
+
// when they leave the canvas to keep the animation visually light.
|
|
144
|
+
const canvas = document.getElementById('canvas-hearts');
|
|
145
|
+
const context = canvas.getContext('2d');
|
|
146
|
+
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
147
|
+
|
|
148
|
+
canvas.width = window.innerWidth * devicePixelRatio;
|
|
149
|
+
canvas.height = window.innerHeight * devicePixelRatio;
|
|
150
|
+
canvas.style.width = `${window.innerWidth}px`;
|
|
151
|
+
canvas.style.height = `${window.innerHeight}px`;
|
|
152
|
+
context.scale(devicePixelRatio, devicePixelRatio);
|
|
153
|
+
|
|
154
|
+
const width = window.innerWidth;
|
|
155
|
+
const height = window.innerHeight;
|
|
156
|
+
const colors = ['#c0392b', '#e74c3c', '#e91e63', '#ff4081', '#ad1457', '#f06292', '#ff6b6b', '#d63031', '#ff8a80'];
|
|
157
|
+
|
|
158
|
+
function drawHeart(radius) {
|
|
159
|
+
context.beginPath();
|
|
160
|
+
context.moveTo(0, -radius * 0.3);
|
|
161
|
+
context.bezierCurveTo(radius, -radius * 1.1, radius * 1.5, radius * 0.4, 0, radius);
|
|
162
|
+
context.bezierCurveTo(-radius * 1.5, radius * 0.4, -radius, -radius * 1.1, 0, -radius * 0.3);
|
|
163
|
+
context.closePath();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const totalParticles = 55;
|
|
167
|
+
const particles = [];
|
|
168
|
+
|
|
169
|
+
for (let index = 0; index < totalParticles; index += 1) {
|
|
170
|
+
const angle = (Math.random() - 0.5) * (Math.PI / 1.5);
|
|
171
|
+
const velocity = 1.2 + Math.random() * 2.2;
|
|
172
|
+
const radius = 8 + Math.random() * 18;
|
|
173
|
+
|
|
174
|
+
particles.push({
|
|
175
|
+
x: width * 0.05 + Math.random() * width * 0.9,
|
|
176
|
+
y: height + radius * 2,
|
|
177
|
+
velocityX: Math.sin(angle) * velocity,
|
|
178
|
+
velocityY: -Math.cos(angle) * velocity,
|
|
179
|
+
radius,
|
|
180
|
+
rotation: (Math.random() - 0.5) * 0.06,
|
|
181
|
+
currentAngle: 0,
|
|
182
|
+
color: colors[Math.floor(Math.random() * colors.length)],
|
|
183
|
+
alpha: 0,
|
|
184
|
+
delay: Math.random() * 2500,
|
|
185
|
+
born: false,
|
|
186
|
+
dead: false
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let startTime = null;
|
|
191
|
+
|
|
192
|
+
function animate(timestamp) {
|
|
193
|
+
if (!startTime) {
|
|
194
|
+
startTime = timestamp;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const elapsed = timestamp - startTime;
|
|
198
|
+
context.clearRect(0, 0, width, height);
|
|
199
|
+
|
|
200
|
+
let allDead = true;
|
|
201
|
+
|
|
202
|
+
particles.forEach((particle) => {
|
|
203
|
+
if (elapsed < particle.delay) {
|
|
204
|
+
allDead = false;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!particle.born) {
|
|
209
|
+
particle.born = true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (particle.dead) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
allDead = false;
|
|
217
|
+
particle.x += particle.velocityX;
|
|
218
|
+
particle.y += particle.velocityY;
|
|
219
|
+
particle.velocityY -= 0.012;
|
|
220
|
+
particle.velocityX *= 0.998;
|
|
221
|
+
particle.currentAngle += particle.rotation;
|
|
222
|
+
|
|
223
|
+
const fraction = 1 - (particle.y / height);
|
|
224
|
+
if (fraction < 0.08) {
|
|
225
|
+
particle.alpha = fraction / 0.08;
|
|
226
|
+
} else if (fraction > 0.55) {
|
|
227
|
+
particle.alpha = Math.max(0, 1 - (fraction - 0.55) / 0.45);
|
|
228
|
+
} else {
|
|
229
|
+
particle.alpha = 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (particle.y < -particle.radius * 3) {
|
|
233
|
+
particle.dead = true;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
context.save();
|
|
238
|
+
context.translate(particle.x, particle.y);
|
|
239
|
+
context.rotate(particle.currentAngle);
|
|
240
|
+
context.globalAlpha = particle.alpha;
|
|
241
|
+
context.shadowColor = particle.color;
|
|
242
|
+
context.shadowBlur = 6;
|
|
243
|
+
context.fillStyle = particle.color;
|
|
244
|
+
drawHeart(particle.radius);
|
|
245
|
+
context.fill();
|
|
246
|
+
context.restore();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (!allDead) {
|
|
250
|
+
window.requestAnimationFrame(animate);
|
|
251
|
+
} else {
|
|
252
|
+
context.clearRect(0, 0, width, height);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
window.requestAnimationFrame(animate);
|
|
257
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { getTranslation } from './i18n.js';
|
|
2
|
+
import { letters } from '../data/lettersLoader.js';
|
|
3
|
+
|
|
4
|
+
const listContainer = document.getElementById('letter-list');
|
|
5
|
+
|
|
6
|
+
const paletteFamilies = [
|
|
7
|
+
{ hue: 18, sat: 54, light: 88, sealHue: 345, sealSat: 76, sealLight: 42 },
|
|
8
|
+
{ hue: 26, sat: 48, light: 86, sealHue: 12, sealSat: 72, sealLight: 38 },
|
|
9
|
+
{ hue: 4, sat: 50, light: 84, sealHue: 5, sealSat: 78, sealLight: 34 },
|
|
10
|
+
{ hue: 38, sat: 42, light: 90, sealHue: 24, sealSat: 66, sealLight: 36 },
|
|
11
|
+
{ hue: 8, sat: 58, light: 86, sealHue: 6, sealSat: 82, sealLight: 30 },
|
|
12
|
+
{ hue: 12, sat: 50, light: 88, sealHue: 11, sealSat: 74, sealLight: 33 },
|
|
13
|
+
{ hue: 22, sat: 46, light: 92, sealHue: 20, sealSat: 68, sealLight: 40 },
|
|
14
|
+
{ hue: 16, sat: 60, light: 84, sealHue: 5, sealSat: 86, sealLight: 32 },
|
|
15
|
+
{ hue: 28, sat: 52, light: 90, sealHue: 15, sealSat: 70, sealLight: 38 },
|
|
16
|
+
{ hue: 14, sat: 44, light: 88, sealHue: 8, sealSat: 80, sealLight: 36 }
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function hashString(value) {
|
|
20
|
+
let hash = 0;
|
|
21
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
22
|
+
hash = ((hash << 5) - hash) + value.charCodeAt(index);
|
|
23
|
+
hash |= 0;
|
|
24
|
+
}
|
|
25
|
+
return Math.abs(hash);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function clamp(value, min, max) {
|
|
29
|
+
return Math.min(Math.max(value, min), max);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function generatePalette(letter) {
|
|
33
|
+
const baseHash = hashString(`${letter.id}|${letter.title}|${letter.date}`);
|
|
34
|
+
const family = paletteFamilies[baseHash % paletteFamilies.length];
|
|
35
|
+
const variant = Math.floor(baseHash / paletteFamilies.length) % 7;
|
|
36
|
+
const hueShift = [0, 14, -12, 20, -18, 26, -22][variant];
|
|
37
|
+
const satShift = [0, 8, -6, 12, -10, 14, -8][variant];
|
|
38
|
+
const lightShift = [0, -8, 8, -5, 6, -10, 10][variant];
|
|
39
|
+
|
|
40
|
+
const envelope = `hsl(${clamp(family.hue + hueShift, 0, 360)}, ${clamp(family.sat + satShift, 42, 72)}%, ${clamp(family.light + lightShift, 78, 92)}%)`;
|
|
41
|
+
const flap = `hsl(${clamp(family.hue + hueShift + 2, 0, 360)}, ${clamp(family.sat + satShift + 8, 50, 82)}%, ${clamp(family.light - 18 + lightShift, 60, 76)}%)`;
|
|
42
|
+
const shadow = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 12, 30, 58)}%, ${clamp(family.light - 36 + lightShift, 42, 58)}%)`;
|
|
43
|
+
const base = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 10, 30, 58)}%, ${clamp(family.light - 30 + lightShift, 44, 62)}%)`;
|
|
44
|
+
const seal = `hsl(${clamp(family.sealHue + hueShift, 0, 360)}, ${clamp(family.sealSat + satShift, 68, 92)}%, ${clamp(family.sealLight + lightShift * 0.8, 28, 46)}%)`;
|
|
45
|
+
const text = `hsl(${family.hue}, 22%, 18%)`;
|
|
46
|
+
|
|
47
|
+
return { envelope, flap, shadow, base, seal, text };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderLetters() {
|
|
51
|
+
if (!listContainer) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const tapToOpenText = getTranslation('TAP_TO_OPEN');
|
|
56
|
+
const orderedLetters = [...letters].reverse();
|
|
57
|
+
|
|
58
|
+
orderedLetters.forEach((letter) => {
|
|
59
|
+
const palette = generatePalette(letter);
|
|
60
|
+
const link = document.createElement('a');
|
|
61
|
+
link.href = `./src/views/letters.view.html?id=${letter.id}`;
|
|
62
|
+
link.className = 'envelope-link';
|
|
63
|
+
|
|
64
|
+
link.innerHTML = `
|
|
65
|
+
<div class="envelope-wrap">
|
|
66
|
+
<div class="envelope-sombra"></div>
|
|
67
|
+
<div class="envelope" style="background:${palette.envelope}; --envelope-sombra:${palette.shadow};">
|
|
68
|
+
<div class="env-corpo-svg">
|
|
69
|
+
<svg viewBox="0 0 100 63" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
|
70
|
+
<polygon points="0,0 0,63 50,31" fill="${palette.shadow}" />
|
|
71
|
+
<polygon points="100,0 100,63 50,31" fill="${palette.shadow}" />
|
|
72
|
+
<polygon points="0,63 100,63 50,31" fill="${palette.base}" />
|
|
73
|
+
</svg>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="env-aba" style="border-top-color:${palette.flap};">
|
|
77
|
+
<svg viewBox="0 0 100 44" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
|
78
|
+
<polygon points="0,0 100,0 50,44" fill="${palette.flap}" />
|
|
79
|
+
</svg>
|
|
80
|
+
<div class="env-lacre" style="background:${palette.seal};">♡</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div class="envelope-centro">
|
|
84
|
+
<span class="envelope-data" style="color:${palette.text};">${letter.date}</span>
|
|
85
|
+
<span class="envelope-titulo" style="color:${palette.text};">${letter.title}</span>
|
|
86
|
+
<span class="envelope-hint" style="color:${palette.text};">${tapToOpenText}</span>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="envelope-canto tl"></div>
|
|
89
|
+
<div class="envelope-canto tr"></div>
|
|
90
|
+
<div class="envelope-canto bl"></div>
|
|
91
|
+
<div class="envelope-canto br"></div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
listContainer.appendChild(link);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
renderLetters();
|
|
101
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const REQUEST_TIMEOUT = 10_000;
|
|
2
|
+
|
|
3
|
+
export async function getDocumentText(documentUrl) {
|
|
4
|
+
if (!documentUrl) {
|
|
5
|
+
throw new Error("Google Docs URL is required.");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let urlObject;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
urlObject = new URL(documentUrl);
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error("Invalid Google Docs URL.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const pathParts = urlObject.pathname.split("/");
|
|
17
|
+
const documentIndex = pathParts.indexOf("d");
|
|
18
|
+
|
|
19
|
+
if (documentIndex === -1 || !pathParts[documentIndex + 1]) {
|
|
20
|
+
throw new Error("Invalid Google Docs URL.");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const documentId = pathParts[documentIndex + 1];
|
|
24
|
+
const tabId = urlObject.searchParams.get("tab");
|
|
25
|
+
|
|
26
|
+
if (!tabId) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Could not find a Google Docs tab in the URL."
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const exportUrl =
|
|
33
|
+
`https://docs.google.com/document/d/${documentId}/export` +
|
|
34
|
+
`?format=txt&tab=${encodeURIComponent(tabId)}`;
|
|
35
|
+
|
|
36
|
+
let response;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
response = await fetch(exportUrl, {
|
|
40
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT)
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error.name === "TimeoutError") {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"Google Docs request timed out. " +
|
|
46
|
+
"Make sure the document is publicly accessible."
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Could not connect to Google Docs: ${error.message}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Google Docs returned HTTP ${response.status}. ` +
|
|
58
|
+
"Make sure the document is publicly accessible."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const text = await response.text();
|
|
63
|
+
|
|
64
|
+
const content = text
|
|
65
|
+
.replace(/\r\n/g, "\n")
|
|
66
|
+
.replace(/\r/g, "\n")
|
|
67
|
+
.replace(/\s+$/, "");
|
|
68
|
+
|
|
69
|
+
if (!content) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"The Google Docs tab does not contain any text."
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return content;
|
|
76
|
+
}
|