moracarta 2.1.1 → 2.1.3

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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/moracarta)](https://www.npmjs.com/package/moracarta)
6
6
  [![npm downloads](https://img.shields.io/npm/dt/moracarta)](https://www.npmjs.com/package/moracarta)
7
7
 
8
- 🌐 **[Live Demo](https://moracarta.pages.dev/)**
8
+ 🌐 **[Live Demo - Latest Stable Branch](https://moracarta.pages.dev/)** | 🛠️ **[Live Demo - Development Branch](https://development.moracarta.pages.dev/)**
9
9
 
10
10
  ## Features
11
11
 
@@ -21,10 +21,16 @@
21
21
 
22
22
  ```bash
23
23
  npm i moracarta
24
- moracarta setup
25
- moracarta build
26
- moracarta deploy
24
+ npx moracarta setup
25
+ npx moracarta build
26
+ npx moracarta deploy
27
27
  ```
28
+ ### What each command does
29
+
30
+ - `npm i moracarta` installs Moracarta as a dependency in your project.
31
+ - `npx moracarta setup` sets up Moracarta and lets you configure the website.
32
+ - `npx moracarta build` builds your website using your configured letters and settings.
33
+ - `npx moracarta deploy` deploys the finished website for free.
28
34
 
29
35
  ### App Demo
30
36
  <p align="center">
@@ -35,7 +41,7 @@ moracarta deploy
35
41
  ### Setup Demo
36
42
 
37
43
  <img src="public/assets/images/setup-demo.png" width="100%">
38
- <br><br>
44
+ <br>
39
45
 
40
46
  ## That's it.
41
47
 
package/index.html CHANGED
@@ -26,6 +26,6 @@
26
26
  <p data-variable="YOUR_NAME"></p>
27
27
  </div>
28
28
 
29
- <script type="module" src="src/scripts/main.script.js"></script>
30
- <script type="module" src="src/scripts/data.variable.js"></script>
29
+ <script type="module" src="src/scripts/main.js"></script>
30
+ <script type="module" src="src/scripts/templateBinding.js"></script>
31
31
  </html>
package/package.json CHANGED
@@ -1,26 +1,38 @@
1
1
  {
2
2
  "name": "moracarta",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "type": "module",
5
5
  "description": "",
6
+ "license": "MIT",
6
7
  "main": "index.js",
8
+ "keywords": [
9
+ "cli",
10
+ "scaffold",
11
+ "static-site",
12
+ "cloudflare",
13
+ "wrangler",
14
+ "gift",
15
+ "envelope",
16
+ "letters",
17
+ "romantic",
18
+ "html-css-js"
19
+ ],
7
20
  "files": [
8
21
  "index.html",
9
22
  "vite.config.mjs",
10
23
  "public",
11
24
  "src/commands",
12
25
  "src/config/fonts.js",
13
- "src/data/letters.example.js",
14
- "src/data/lettersLoader.js",
15
- "src/i18n",
26
+ "src/content/letters.example.js",
27
+ "src/loaders/lettersLoader.js",
16
28
  "src/scripts",
17
29
  "src/services",
18
30
  "src/styles",
19
31
  "src/views"
20
32
  ],
21
- "postinstall": "node src/commands/postinstall.js",
33
+ "postinstall": "node src/commands/postInstall.js",
22
34
  "bin": {
23
- "moracarta": "src/commands/cliConfig.js"
35
+ "moracarta": "src/config/cliConfig.js"
24
36
  },
25
37
  "scripts": {
26
38
  "setup": "node src/commands/setup.js",
@@ -34,13 +46,11 @@
34
46
  "type": "git",
35
47
  "url": "git+https://github.com/WesleyHanauer/moracarta.git"
36
48
  },
37
- "keywords": [],
38
- "author": "",
39
- "license": "ISC",
49
+ "author": "Wesley Hanauer",
40
50
  "bugs": {
41
51
  "url": "https://github.com/WesleyHanauer/moracarta/issues"
42
52
  },
43
- "homepage": "https://github.com/WesleyHanauer/moracarta#readme",
53
+ "homepage": "https://moracarta.pages.dev",
44
54
  "dependencies": {
45
55
  "@inquirer/prompts": "^8.6.0",
46
56
  "vite": "^8.2.2",
@@ -11,7 +11,7 @@ import { getDocumentText } from "../services/googleDocs.js";
11
11
  const DATA_DIRECTORY = resolve(
12
12
  process.cwd(),
13
13
  "src",
14
- "data"
14
+ "content"
15
15
  );
16
16
 
17
17
  const LETTERS_FILE = resolve(
@@ -76,7 +76,7 @@ ${letterObject}
76
76
 
77
77
  if (closingIndex === -1) {
78
78
  throw new Error(
79
- "Could not find the end of the letters array in src/data/letters.js."
79
+ "Could not find the end of the letters array in src/content/letters.js."
80
80
  );
81
81
  }
82
82
 
@@ -4,12 +4,12 @@ import { execSync } from "node:child_process";
4
4
  import { resolve } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
 
7
- const configPath = resolve(process.cwd(), "src", "config", "globalVariables.js");
7
+ const dataPath = resolve(process.cwd(), "src", "config", "globalVariables.js");
8
8
 
9
9
  let globalVariables;
10
10
 
11
11
  try {
12
- ({ default: globalVariables } = await import(pathToFileURL(configPath).href));
12
+ ({ default: globalVariables } = await import(pathToFileURL(dataPath).href));
13
13
  } catch (error) {
14
14
  if (error.code === "ERR_MODULE_NOT_FOUND") {
15
15
  console.error("\n✗ No configuration found. Run moracarta setup first.");
@@ -7,7 +7,7 @@ import { resolve } from "node:path";
7
7
  const LETTERS_FILE = resolve(
8
8
  process.cwd(),
9
9
  "src",
10
- "data",
10
+ "content",
11
11
  "letters.js"
12
12
  );
13
13
 
@@ -32,7 +32,7 @@ function extractLetters(lettersFile) {
32
32
 
33
33
  if (!match) {
34
34
  throw new Error(
35
- "Could not find the letters array in src/data/letters.js."
35
+ "Could not find the letters array in src/content/letters.js."
36
36
  );
37
37
  }
38
38
 
@@ -11,17 +11,6 @@ const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = dirname(__filename);
12
12
  const PACKAGE_ROOT = resolve(__dirname, "..", "..");
13
13
 
14
- /** TODO
15
- const LANGUAGE = await select({
16
- message: "What is the main language of the application?",
17
- choices: [
18
- { name: "English", value: "en" },
19
- { name: "Português (Brasil)", value: "pt-BR" }
20
- ]
21
- });
22
- */
23
- const LANGUAGE = "en";
24
-
25
14
  const PROJECT_NAME = await input({
26
15
  message: "What should your project be called? (Cloudflare project name, do not use 'moracarta')",
27
16
  default: "romantic-gift",
@@ -137,8 +126,6 @@ const config = `/**
137
126
  * This file was generated automatically by Moracarta setup.
138
127
  */
139
128
 
140
- const LANGUAGE = ${JSON.stringify(LANGUAGE)};
141
-
142
129
  const PROJECT_NAME = ${JSON.stringify(PROJECT_NAME)};
143
130
  const TITLE = ${JSON.stringify(TITLE)};
144
131
 
@@ -155,7 +142,6 @@ const CLOSED_LETTER_TEXT_BOTTOM_LINE =
155
142
  ${JSON.stringify(CLOSED_LETTER_TEXT_BOTTOM_LINE)};
156
143
 
157
144
  const globalVariables = {
158
- LANGUAGE,
159
145
  PROJECT_NAME,
160
146
  TITLE,
161
147
  YOUR_NAME,
@@ -169,15 +155,15 @@ const globalVariables = {
169
155
  export default globalVariables;
170
156
  `;
171
157
 
172
- const configPath = resolve(
158
+ const dataPath = resolve(
173
159
  process.cwd(),
174
160
  "src",
175
161
  "config",
176
162
  "globalVariables.js"
177
163
  );
178
164
 
179
- await mkdir(dirname(configPath), { recursive: true });
180
- await writeFile(configPath, config, "utf8");
165
+ await mkdir(dirname(dataPath), { recursive: true });
166
+ await writeFile(dataPath, config, "utf8");
181
167
 
182
168
  /**
183
169
  * Files that make up the actual site. These get copied out of the
@@ -189,9 +175,8 @@ const TEMPLATE_ENTRIES = [
189
175
  "vite.config.mjs",
190
176
  "public",
191
177
  "src/config/fonts.js",
192
- "src/data/letters.example.js",
193
- "src/data/lettersLoader.js",
194
- "src/i18n",
178
+ "src/content/letters.example.js",
179
+ "src/loaders/lettersLoader.js",
195
180
  "src/scripts",
196
181
  "src/services",
197
182
  "src/styles",
@@ -253,5 +238,5 @@ await copyTemplateFiles();
253
238
  await ensureModuleType();
254
239
 
255
240
  console.log("\n❤️ Moracarta setup complete!");
256
- console.log(`Configuration saved to: ${configPath}`);
241
+ console.log(`Configuration saved to: ${dataPath}`);
257
242
  console.log("Run moracarta dev to test the application");
@@ -24,5 +24,25 @@ Peter Parker`
24
24
 
25
25
  With all my love,
26
26
  Peter Parker`
27
+ },
28
+ {
29
+ id: 3,
30
+ date: 'Aug 31, 2026',
31
+ title: 'Custom Letter Title 3',
32
+ content: `Dear Mary Jane,
33
+ Sometimes I wonder how I got so lucky to have you in my life. Between all the chaos, the late nights, and everything I have to keep hidden, you're always the person who makes me feel like I can just be Peter. You make even the hardest days feel a little lighter, and I hope you know how much that means to me.
34
+ I know I don't always have the time or the words to explain how I feel, but I want you to know that you're always on my mind. No matter how far I swing, no matter how crazy things get, there's always a part of me that wants to come home to you. You're not just someone I love, MJ. You're my favorite part of being Peter Parker.
35
+ Love,
36
+ Peter Parker`
37
+ },
38
+ {
39
+ id: 4,
40
+ date: 'Aug 31, 2026',
41
+ title: 'Custom Letter Title 4',
42
+ content: `Dear MJ,
43
+ There are so many things I wish I could tell you, but somehow every time I try, the words never seem to be enough. You're the person who sees me when I don't even know how to see myself. When everything feels overwhelming, just hearing your voice reminds me that maybe things aren't as hopeless as they seem.
44
+ I know I've made mistakes, and I know I've kept things from you that I probably shouldn't have. But through all of it, you've stayed by my side. You make me want to be better—not because you expect me to be perfect, but because being with you makes me want to become someone worthy of the way you believe in me.
45
+ Love,
46
+ Peter`
27
47
  }
28
48
  ];
@@ -0,0 +1,7 @@
1
+ const modules = import.meta.glob("../content/letters*.js", {
2
+ eager: true,
3
+ import: "letters",
4
+ });
5
+
6
+ export const letters =
7
+ modules["../content/letters.js"] ?? modules["../content/letters.example.js"];
@@ -1,5 +1,4 @@
1
- import { getTranslation } from './i18n.js';
2
- import { letters } from '../data/lettersLoader.js';
1
+ import { letters } from '../loaders/lettersLoader.js';
3
2
 
4
3
  console.log('letters.script loaded');
5
4
 
@@ -99,17 +98,17 @@ if (!letter) {
99
98
  const previousLetter = currentIndex > 0 ? sortedLetters[currentIndex - 1] : null;
100
99
  const nextLetter = currentIndex < sortedLetters.length - 1 ? sortedLetters[currentIndex + 1] : null;
101
100
 
102
- document.getElementById('letter-counter').textContent = `${currentIndex + 1} ${getTranslation('OF')} ${sortedLetters.length}`;
101
+ document.getElementById('letter-counter').textContent = `${currentIndex + 1} Of ${sortedLetters.length}`;
103
102
 
104
103
  if (previousLetter) {
105
104
  const previousButton = document.getElementById('nav-previous');
106
- previousButton.href = `./letters.view.html?id=${previousLetter.id}`;
105
+ previousButton.href = `./letters.html?id=${previousLetter.id}`;
107
106
  previousButton.classList.remove('invisible');
108
107
  }
109
108
 
110
109
  if (nextLetter) {
111
110
  const nextButton = document.getElementById('nav-next');
112
- nextButton.href = `./letters.view.html?id=${nextLetter.id}`;
111
+ nextButton.href = `./letters.html?id=${nextLetter.id}`;
113
112
  nextButton.classList.remove('invisible');
114
113
  }
115
114
 
@@ -0,0 +1,256 @@
1
+ import { letters } from '../loaders/lettersLoader.js';
2
+
3
+ console.log('letters.script loaded');
4
+
5
+ const params = new URLSearchParams(window.location.search);
6
+ const requestedId = parseInt(params.get('id'), 10);
7
+ const letter = letters.find((entry) => entry.id === requestedId);
8
+
9
+ // A set of base color families used to generate a unique yet consistent
10
+ // envelope palette for each letter based on its id, title, and date.
11
+ const paletteFamilies = [
12
+ { hue: 18, sat: 54, light: 88, sealHue: 345, sealSat: 76, sealLight: 42 },
13
+ { hue: 26, sat: 48, light: 86, sealHue: 12, sealSat: 72, sealLight: 38 },
14
+ { hue: 4, sat: 50, light: 84, sealHue: 5, sealSat: 78, sealLight: 34 },
15
+ { hue: 38, sat: 42, light: 90, sealHue: 24, sealSat: 66, sealLight: 36 },
16
+ { hue: 8, sat: 58, light: 86, sealHue: 6, sealSat: 82, sealLight: 30 },
17
+ { hue: 12, sat: 50, light: 88, sealHue: 11, sealSat: 74, sealLight: 33 },
18
+ { hue: 22, sat: 46, light: 92, sealHue: 20, sealSat: 68, sealLight: 40 },
19
+ { hue: 16, sat: 60, light: 84, sealHue: 5, sealSat: 86, sealLight: 32 },
20
+ { hue: 28, sat: 52, light: 90, sealHue: 15, sealSat: 70, sealLight: 38 },
21
+ { hue: 14, sat: 44, light: 88, sealHue: 8, sealSat: 80, sealLight: 36 }
22
+ ];
23
+
24
+ function hashString(value) {
25
+ // Simple deterministic string hash used to select a palette and variant.
26
+ // This keeps each letter appearance stable across page loads.
27
+ let hash = 0;
28
+ for (let index = 0; index < value.length; index += 1) {
29
+ hash = ((hash << 5) - hash) + value.charCodeAt(index);
30
+ hash |= 0;
31
+ }
32
+ return Math.abs(hash);
33
+ }
34
+
35
+ function clamp(value, min, max) {
36
+ return Math.min(Math.max(value, min), max);
37
+ }
38
+
39
+ function generatePalette(letterItem) {
40
+ // Build a deterministic palette based on the letter's identity.
41
+ // The base hash selects one of the palette families, while the variant
42
+ // arrays add subtle shifts to keep similar letters visually distinct.
43
+ const baseHash = hashString(`${letterItem.id}|${letterItem.title}|${letterItem.date}`);
44
+ const family = paletteFamilies[baseHash % paletteFamilies.length];
45
+ const variant = Math.floor(baseHash / paletteFamilies.length) % 7;
46
+ const hueShift = [0, 14, -12, 20, -18, 26, -22][variant];
47
+ const satShift = [0, 8, -6, 12, -10, 14, -8][variant];
48
+ const lightShift = [0, -8, 8, -5, 6, -10, 10][variant];
49
+
50
+ const envelope = `hsl(${clamp(family.hue + hueShift, 0, 360)}, ${clamp(family.sat + satShift, 42, 72)}%, ${clamp(family.light + lightShift, 78, 92)}%)`;
51
+ const flap = `hsl(${clamp(family.hue + hueShift + 2, 0, 360)}, ${clamp(family.sat + satShift + 8, 50, 82)}%, ${clamp(family.light - 18 + lightShift, 60, 76)}%)`;
52
+ const shadow = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 12, 30, 58)}%, ${clamp(family.light - 36 + lightShift, 42, 58)}%)`;
53
+ const base = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 10, 30, 58)}%, ${clamp(family.light - 30 + lightShift, 44, 62)}%)`;
54
+ const seal = `hsl(${clamp(family.sealHue + hueShift, 0, 360)}, ${clamp(family.sealSat + satShift, 68, 92)}%, ${clamp(family.sealLight + lightShift * 0.8, 28, 46)}%)`;
55
+ const text = `hsl(${family.hue}, 22%, 18%)`;
56
+
57
+ return { envelope, flap, shadow, base, seal, text };
58
+ }
59
+
60
+ function applyEnvelopePalette(letterItem) {
61
+ const palette = generatePalette(letterItem);
62
+ document.querySelector('.env-fundo').style.background = palette.envelope;
63
+
64
+ const flapPolygon = document.querySelector('.env-aba svg polygon');
65
+ if (flapPolygon) {
66
+ flapPolygon.setAttribute('fill', palette.flap);
67
+ }
68
+
69
+ const sidePolygons = document.querySelectorAll('.env-corpo-svg svg polygon');
70
+ if (sidePolygons.length === 3) {
71
+ sidePolygons[0].setAttribute('fill', palette.shadow);
72
+ sidePolygons[1].setAttribute('fill', palette.shadow);
73
+ sidePolygons[2].setAttribute('fill', palette.base);
74
+ }
75
+ }
76
+
77
+ if (!letter) {
78
+ document.getElementById('error-message').style.display = 'block';
79
+ document.getElementById('envelope-wrap').style.display = 'none';
80
+ document.getElementById('letter-wrap').style.display = 'none';
81
+ document.getElementById('letter-nav').style.display = 'none';
82
+ } else {
83
+ document.title = letter.title;
84
+
85
+ document.getElementById('letter-date').textContent = letter.date;
86
+ document.getElementById('letter-title').textContent = letter.title;
87
+ document.getElementById('letter-body').innerHTML = letter.content
88
+ .split(/\n\s*\n/)
89
+ .filter(paragraph => paragraph.trim())
90
+ .map(paragraph => `<p>${paragraph.trimEnd()}</p>`)
91
+ .join('');
92
+
93
+ applyEnvelopePalette(letter);
94
+ document.querySelector('.envelope-stage').classList.add('ready');
95
+
96
+ const sortedLetters = letters.slice().sort((first, second) => first.id - second.id);
97
+ const currentIndex = sortedLetters.findIndex((entry) => entry.id === requestedId);
98
+ const previousLetter = currentIndex > 0 ? sortedLetters[currentIndex - 1] : null;
99
+ const nextLetter = currentIndex < sortedLetters.length - 1 ? sortedLetters[currentIndex + 1] : null;
100
+
101
+ document.getElementById('letter-counter').textContent = `${currentIndex + 1} Of ${sortedLetters.length}`;
102
+
103
+ if (previousLetter) {
104
+ const previousButton = document.getElementById('nav-previous');
105
+ previousButton.href = `./letters.html?id=${previousLetter.id}`;
106
+ previousButton.classList.remove('invisible');
107
+ }
108
+
109
+ if (nextLetter) {
110
+ const nextButton = document.getElementById('nav-next');
111
+ nextButton.href = `./letters.html?id=${nextLetter.id}`;
112
+ nextButton.classList.remove('invisible');
113
+ }
114
+
115
+ startHeartAnimation();
116
+
117
+ // Animate the envelope opening, sealing, and letter reveal in timed stages.
118
+ window.setTimeout(() => {
119
+ document.getElementById('flap').classList.add('open');
120
+ document.getElementById('seal').classList.add('fading');
121
+ }, 400);
122
+
123
+ window.setTimeout(() => {
124
+ document.getElementById('flap').classList.add('behind');
125
+ }, 1050);
126
+
127
+ window.setTimeout(() => {
128
+ document.getElementById('paper').classList.add('leaving');
129
+ }, 900);
130
+
131
+ window.setTimeout(() => {
132
+ document.getElementById('letter-wrap').classList.add('visible');
133
+ document.getElementById('letter-nav').classList.add('visible');
134
+ document.getElementById('envelope-wrap').style.display = 'none';
135
+
136
+ }, 2100);
137
+ }
138
+
139
+ function startHeartAnimation() {
140
+ // Floating heart particle animation for the letter view background.
141
+ // Particles are born after a delay, float upward, fade at the top, and die
142
+ // when they leave the canvas to keep the animation visually light.
143
+ const canvas = document.getElementById('canvas-hearts');
144
+ const context = canvas.getContext('2d');
145
+ const devicePixelRatio = window.devicePixelRatio || 1;
146
+
147
+ canvas.width = window.innerWidth * devicePixelRatio;
148
+ canvas.height = window.innerHeight * devicePixelRatio;
149
+ canvas.style.width = `${window.innerWidth}px`;
150
+ canvas.style.height = `${window.innerHeight}px`;
151
+ context.scale(devicePixelRatio, devicePixelRatio);
152
+
153
+ const width = window.innerWidth;
154
+ const height = window.innerHeight;
155
+ const colors = ['#c0392b', '#e74c3c', '#e91e63', '#ff4081', '#ad1457', '#f06292', '#ff6b6b', '#d63031', '#ff8a80'];
156
+
157
+ function drawHeart(radius) {
158
+ context.beginPath();
159
+ context.moveTo(0, -radius * 0.3);
160
+ context.bezierCurveTo(radius, -radius * 1.1, radius * 1.5, radius * 0.4, 0, radius);
161
+ context.bezierCurveTo(-radius * 1.5, radius * 0.4, -radius, -radius * 1.1, 0, -radius * 0.3);
162
+ context.closePath();
163
+ }
164
+
165
+ const totalParticles = 55;
166
+ const particles = [];
167
+
168
+ for (let index = 0; index < totalParticles; index += 1) {
169
+ const angle = (Math.random() - 0.5) * (Math.PI / 1.5);
170
+ const velocity = 1.2 + Math.random() * 2.2;
171
+ const radius = 8 + Math.random() * 18;
172
+
173
+ particles.push({
174
+ x: width * 0.05 + Math.random() * width * 0.9,
175
+ y: height + radius * 2,
176
+ velocityX: Math.sin(angle) * velocity,
177
+ velocityY: -Math.cos(angle) * velocity,
178
+ radius,
179
+ rotation: (Math.random() - 0.5) * 0.06,
180
+ currentAngle: 0,
181
+ color: colors[Math.floor(Math.random() * colors.length)],
182
+ alpha: 0,
183
+ delay: Math.random() * 2500,
184
+ born: false,
185
+ dead: false
186
+ });
187
+ }
188
+
189
+ let startTime = null;
190
+
191
+ function animate(timestamp) {
192
+ if (!startTime) {
193
+ startTime = timestamp;
194
+ }
195
+
196
+ const elapsed = timestamp - startTime;
197
+ context.clearRect(0, 0, width, height);
198
+
199
+ let allDead = true;
200
+
201
+ particles.forEach((particle) => {
202
+ if (elapsed < particle.delay) {
203
+ allDead = false;
204
+ return;
205
+ }
206
+
207
+ if (!particle.born) {
208
+ particle.born = true;
209
+ }
210
+
211
+ if (particle.dead) {
212
+ return;
213
+ }
214
+
215
+ allDead = false;
216
+ particle.x += particle.velocityX;
217
+ particle.y += particle.velocityY;
218
+ particle.velocityY -= 0.012;
219
+ particle.velocityX *= 0.998;
220
+ particle.currentAngle += particle.rotation;
221
+
222
+ const fraction = 1 - (particle.y / height);
223
+ if (fraction < 0.08) {
224
+ particle.alpha = fraction / 0.08;
225
+ } else if (fraction > 0.55) {
226
+ particle.alpha = Math.max(0, 1 - (fraction - 0.55) / 0.45);
227
+ } else {
228
+ particle.alpha = 1;
229
+ }
230
+
231
+ if (particle.y < -particle.radius * 3) {
232
+ particle.dead = true;
233
+ return;
234
+ }
235
+
236
+ context.save();
237
+ context.translate(particle.x, particle.y);
238
+ context.rotate(particle.currentAngle);
239
+ context.globalAlpha = particle.alpha;
240
+ context.shadowColor = particle.color;
241
+ context.shadowBlur = 6;
242
+ context.fillStyle = particle.color;
243
+ drawHeart(particle.radius);
244
+ context.fill();
245
+ context.restore();
246
+ });
247
+
248
+ if (!allDead) {
249
+ window.requestAnimationFrame(animate);
250
+ } else {
251
+ context.clearRect(0, 0, width, height);
252
+ }
253
+ }
254
+
255
+ window.requestAnimationFrame(animate);
256
+ }
@@ -1,5 +1,4 @@
1
- import { getTranslation } from './i18n.js';
2
- import { letters } from '../data/lettersLoader.js';
1
+ import { letters } from '../loaders/lettersLoader.js';
3
2
 
4
3
  const listContainer = document.getElementById('letter-list');
5
4
 
@@ -52,13 +51,12 @@ function renderLetters() {
52
51
  return;
53
52
  }
54
53
 
55
- const tapToOpenText = getTranslation('TAP_TO_OPEN');
56
54
  const orderedLetters = [...letters].reverse();
57
55
 
58
56
  orderedLetters.forEach((letter) => {
59
57
  const palette = generatePalette(letter);
60
58
  const link = document.createElement('a');
61
- link.href = `./src/views/letters.view.html?id=${letter.id}`;
59
+ link.href = `./src/views/letters.html?id=${letter.id}`;
62
60
  link.className = 'envelope-link';
63
61
 
64
62
  link.innerHTML = `
@@ -83,7 +81,7 @@ function renderLetters() {
83
81
  <div class="envelope-centro">
84
82
  <span class="envelope-data" style="color:${palette.text};">${letter.date}</span>
85
83
  <span class="envelope-titulo" style="color:${palette.text};">${letter.title}</span>
86
- <span class="envelope-hint" style="color:${palette.text};">${tapToOpenText}</span>
84
+ <span class="envelope-hint" style="color:${palette.text};">Tap to open</span>
87
85
  </div>
88
86
  <div class="envelope-canto tl"></div>
89
87
  <div class="envelope-canto tr"></div>
@@ -6,7 +6,7 @@ document.documentElement.style.setProperty(
6
6
  fonts[globalVariables.FONT] || fonts.handwritten
7
7
  );
8
8
 
9
- // Data variables are injected from `src/config/globalVariables.js` into
9
+ // Template variables are injected from `src/config/globalVariables.js` into
10
10
  // HTML elements using the `data-variable` attribute.
11
11
  // HTML entities are preserved when the value contains markup characters.
12
12
  document.querySelectorAll("[data-variable]").forEach(element => {
@@ -15,11 +15,11 @@
15
15
  <div class="back-link-wrap">
16
16
  <a href="../../index.html" class="back-link">
17
17
  <span class="back-link-arrow">&#8592;</span>
18
- <span data-i18n="RETURN"></span>
18
+ <span>Return</span>
19
19
  </a>
20
20
  </div>
21
21
 
22
- <div id="error-message" class="error" style="display:none"><p data-i18n="LETTER_NOT_FOUND"></p></div>
22
+ <div id="error-message" class="error" style="display:none"><p>Letter not found</p></div>
23
23
 
24
24
  <div class="envelope-animation-wrap" id="envelope-wrap">
25
25
  <div class="envelope-stage">
@@ -72,12 +72,11 @@
72
72
  </div>
73
73
 
74
74
  <nav class="letter-nav" id="letter-nav">
75
- <a id="nav-previous" class="nav-button invisible" href="#">&#8592; <p data-i18n="PREVIOUS"></p></a>
75
+ <a id="nav-previous" class="nav-button invisible" href="#">&#8592; <p>Previous</p></a>
76
76
  <span class="nav-counter" id="letter-counter"></span>
77
- <a id="nav-next" class="nav-button invisible" href="#"><p data-i18n="NEXT"></p> &#8594;</a>
77
+ <a id="nav-next" class="nav-button invisible" href="#"><p>Next</p> &#8594;</a>
78
78
  </nav>
79
79
 
80
- <script type="module" src="../scripts/letters.script.js"></script>
81
- <script type="module" src="../scripts/i18n.js"></script>
82
- <script type="module" src="../scripts/data.variable.js"></script>
80
+ <script type="module" src="../scripts/lettersPage.js"></script>
81
+ <script type="module" src="../scripts/templateBinding.js"></script>
83
82
  </html>
package/vite.config.mjs CHANGED
@@ -9,7 +9,7 @@ export default defineConfig({
9
9
  rollupOptions: {
10
10
  input: {
11
11
  main: resolve(import.meta.dirname, 'index.html'),
12
- letter: resolve(import.meta.dirname, 'src/views/letters.view.html')
12
+ letter: resolve(import.meta.dirname, 'src/views/letters.html')
13
13
  }
14
14
  }
15
15
  }
@@ -1,7 +0,0 @@
1
- const modules = import.meta.glob("./letters*.js", {
2
- eager: true,
3
- import: "letters",
4
- });
5
-
6
- export const letters =
7
- modules["./letters.js"] ?? modules["./letters.example.js"];
package/src/i18n/de.js DELETED
@@ -1,8 +0,0 @@
1
- export default {
2
- TAP_TO_OPEN: "Zum Öffnen tippen",
3
- RETURN: "Zurück",
4
- PREVIOUS: "Vorherige",
5
- NEXT: "Nächste",
6
- OF: "von",
7
- LETTER_NOT_FOUND: "Brief nicht gefunden."
8
- }
package/src/i18n/en.js DELETED
@@ -1,8 +0,0 @@
1
- export default{
2
- TAP_TO_OPEN: "Tap to open",
3
- RETURN: "Return",
4
- PREVIOUS: "Previous",
5
- NEXT: "Next",
6
- OF: "of",
7
- LETTER_NOT_FOUND: "Letter not found."
8
- }
package/src/i18n/es.js DELETED
@@ -1,8 +0,0 @@
1
- export default {
2
- TAP_TO_OPEN: "Toca para abrir",
3
- RETURN: "Volver",
4
- PREVIOUS: "Anterior",
5
- NEXT: "Siguiente",
6
- OF: "de",
7
- LETTER_NOT_FOUND: "Carta no encontrada."
8
- }
package/src/i18n/fr.js DELETED
@@ -1,8 +0,0 @@
1
- export default {
2
- TAP_TO_OPEN: "Touchez pour ouvrir",
3
- RETURN: "Retour",
4
- PREVIOUS: "Précédente",
5
- NEXT: "Suivante",
6
- OF: "sur",
7
- LETTER_NOT_FOUND: "Lettre introuvable."
8
- }
package/src/i18n/it.js DELETED
@@ -1,8 +0,0 @@
1
- export default {
2
- TAP_TO_OPEN: "Tocca per aprire",
3
- RETURN: "Indietro",
4
- PREVIOUS: "Precedente",
5
- NEXT: "Successiva",
6
- OF: "di",
7
- LETTER_NOT_FOUND: "Lettera non trovata."
8
- }
package/src/i18n/ja.js DELETED
@@ -1,8 +0,0 @@
1
- export default {
2
- TAP_TO_OPEN: "タップして開く",
3
- RETURN: "戻る",
4
- PREVIOUS: "前へ",
5
- NEXT: "次へ",
6
- OF: "の",
7
- LETTER_NOT_FOUND: "手紙が見つかりません。"
8
- }
package/src/i18n/ko.js DELETED
@@ -1,8 +0,0 @@
1
- export default {
2
- TAP_TO_OPEN: "탭하여 열기",
3
- RETURN: "돌아가기",
4
- PREVIOUS: "이전",
5
- NEXT: "다음",
6
- OF: "중",
7
- LETTER_NOT_FOUND: "편지를 찾을 수 없습니다."
8
- }
package/src/i18n/nl.js DELETED
File without changes
package/src/i18n/pr.js DELETED
File without changes
package/src/i18n/pt-BR.js DELETED
@@ -1,8 +0,0 @@
1
- export default{
2
- TAP_TO_OPEN: "Toque para abrir",
3
- RETURN: "Voltar",
4
- PREVIOUS: "Anterior",
5
- NEXT: "Próxima",
6
- OF: "de",
7
- LETTER_NOT_FOUND: "Carta não encontrada."
8
- }
package/src/i18n/ru.js DELETED
@@ -1,8 +0,0 @@
1
- export default {
2
- TAP_TO_OPEN: "Нажмите, чтобы открыть",
3
- RETURN: "Назад",
4
- PREVIOUS: "Предыдущая",
5
- NEXT: "Следующая",
6
- OF: "из",
7
- LETTER_NOT_FOUND: "Письмо не найдено."
8
- }
package/src/i18n/tl.js DELETED
File without changes
package/src/i18n/zh.js DELETED
@@ -1,8 +0,0 @@
1
- export default {
2
- TAP_TO_OPEN: "点击打开",
3
- RETURN: "返回",
4
- PREVIOUS: "上一封",
5
- NEXT: "下一封",
6
- OF: "共",
7
- LETTER_NOT_FOUND: "未找到信件。"
8
- }
@@ -1,67 +0,0 @@
1
- import globalVariables from "../config/globalVariables.js";
2
-
3
- import en from "../i18n/en.js";
4
- import ptbr from "../i18n/pt-BR.js";
5
- import es from "../i18n/es.js";
6
- import fr from "../i18n/fr.js";
7
- import de from "../i18n/de.js";
8
- import it from "../i18n/it.js";
9
- import ja from "../i18n/ja.js";
10
- import ko from "../i18n/ko.js";
11
- import zh from "../i18n/zh.js";
12
- import ru from "../i18n/ru.js";
13
-
14
- // Language files are selected by the configured LANGUAGE value.
15
- // Missing translation keys fall back to English, then to the key name.
16
- const translations = {
17
- en,
18
- "pt-BR": ptbr,
19
- es,
20
- fr,
21
- de,
22
- it,
23
- ja,
24
- ko,
25
- zh,
26
- ru
27
- };
28
-
29
- const getCurrentLanguage = () => globalVariables?.LANGUAGE || "en";
30
-
31
- export const getTranslation = (key, ...args) => {
32
- const language = translations[getCurrentLanguage()] || translations.en;
33
-
34
- const translation =
35
- language?.[key] ??
36
- translations.en?.[key] ??
37
- key;
38
-
39
- // Support translated functions such as PROGRESS(current, total)
40
- return typeof translation === "function"
41
- ? translation(...args)
42
- : translation;
43
- };
44
-
45
- export const translateElement = (element) => {
46
- const key = element.dataset.i18n;
47
-
48
- if (key) {
49
- element.textContent = getTranslation(key);
50
- }
51
- };
52
-
53
- document
54
- .querySelectorAll("[data-i18n]")
55
- .forEach(translateElement);
56
-
57
- export const refreshTranslations = () => {
58
- document
59
- .querySelectorAll("[data-i18n]")
60
- .forEach(translateElement);
61
- };
62
-
63
- window.i18n = {
64
- getTranslation,
65
- translateElement,
66
- refreshTranslations
67
- };
File without changes