moracarta 2.1.2 → 2.1.4
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 +8 -3
- package/index.html +34 -30
- package/package.json +21 -17
- package/src/commands/add.js +2 -2
- package/src/commands/deploy.js +2 -2
- package/src/commands/remove.js +2 -2
- package/src/commands/setup.js +33 -32
- package/src/content/letters.example.js +57 -0
- package/src/loaders/lettersLoader.js +13 -0
- package/src/scripts/{main.script.js → generatePalette.js} +11 -61
- package/src/scripts/{letters.script.js → lettersPage.js} +11 -63
- package/src/scripts/main.js +62 -0
- package/src/scripts/{data.variable.js → templateBinding.js} +3 -6
- package/src/styles/letters.css +11 -11
- package/src/styles/main.css +27 -18
- package/src/views/{letters.view.html → letters.html} +12 -13
- package/vite.config.mjs +1 -1
- package/src/data/letters.example.js +0 -28
- package/src/data/lettersLoader.js +0 -7
- package/src/i18n/de.js +0 -8
- package/src/i18n/en.js +0 -8
- package/src/i18n/es.js +0 -8
- package/src/i18n/fr.js +0 -8
- package/src/i18n/it.js +0 -8
- package/src/i18n/ja.js +0 -8
- package/src/i18n/ko.js +0 -8
- package/src/i18n/nl.js +0 -0
- package/src/i18n/pr.js +0 -0
- package/src/i18n/pt-BR.js +0 -8
- package/src/i18n/ru.js +0 -8
- package/src/i18n/tl.js +0 -0
- package/src/i18n/zh.js +0 -8
- package/src/scripts/i18n.js +0 -67
- /package/src/{commands → config}/cliConfig.js +0 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/moracarta)
|
|
6
6
|
[](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
|
|
|
@@ -20,12 +20,17 @@
|
|
|
20
20
|
**And it's as simple as:**
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
npm init -y
|
|
24
23
|
npm i moracarta
|
|
25
24
|
npx moracarta setup
|
|
26
25
|
npx moracarta build
|
|
27
26
|
npx moracarta deploy
|
|
28
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.
|
|
29
34
|
|
|
30
35
|
### App Demo
|
|
31
36
|
<p align="center">
|
|
@@ -36,7 +41,7 @@ npx moracarta deploy
|
|
|
36
41
|
### Setup Demo
|
|
37
42
|
|
|
38
43
|
<img src="public/assets/images/setup-demo.png" width="100%">
|
|
39
|
-
<br
|
|
44
|
+
<br>
|
|
40
45
|
|
|
41
46
|
## That's it.
|
|
42
47
|
|
package/index.html
CHANGED
|
@@ -1,31 +1,35 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
6
|
-
<meta name="robots" content="noindex, nofollow, noarchive">
|
|
7
|
-
<title data-variable="TITLE"></title>
|
|
8
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
-
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300..700;1,300..700&family=EB+Garamond:ital,wght@0,400..800;1,400..800&family=Lora:ital,wght@0,400;1,400&family=Montserrat:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&family=Playfair+Display:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
|
|
11
|
-
<link rel="stylesheet" href="src/styles/main.css">
|
|
12
|
-
</head>
|
|
13
|
-
<body>
|
|
14
|
-
<div class="header">
|
|
15
|
-
<div class="header-ornament">❧ ❦ ☙</div>
|
|
16
|
-
<h1 data-variable="TITLE"></h1>
|
|
17
|
-
<div class="header-line"></div>
|
|
18
|
-
<p class="header-sub love-note" data-variable="TEXT_TOP"></p>
|
|
19
|
-
</div>
|
|
20
|
-
|
|
21
|
-
<div class="letter-list" id="letter-list"></div>
|
|
22
|
-
|
|
23
|
-
<div class="
|
|
24
|
-
<div class="
|
|
25
|
-
<p data-variable="TEXT_BOTTOM"></p>
|
|
26
|
-
<p data-variable="YOUR_NAME"></p>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<
|
|
30
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
6
|
+
<meta name="robots" content="noindex, nofollow, noarchive">
|
|
7
|
+
<title data-variable="TITLE"></title>
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300..700;1,300..700&family=EB+Garamond:ital,wght@0,400..800;1,400..800&family=Lora:ital,wght@0,400;1,400&family=Montserrat:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&family=Playfair+Display:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
|
|
11
|
+
<link rel="stylesheet" href="src/styles/main.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div class="header">
|
|
15
|
+
<div class="header-ornament">❧ ❦ ☙</div>
|
|
16
|
+
<h1 data-variable="TITLE"></h1>
|
|
17
|
+
<div class="header-line"></div>
|
|
18
|
+
<p class="header-sub love-note" data-variable="TEXT_TOP"></p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="letter-list" id="letter-list"></div>
|
|
22
|
+
|
|
23
|
+
<div class="bottom-text">
|
|
24
|
+
<div class="bottom-text-line"></div>
|
|
25
|
+
<p data-variable="TEXT_BOTTOM"></p>
|
|
26
|
+
<p data-variable="YOUR_NAME"></p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<footer class="footer">
|
|
30
|
+
<a id="footer-link" class="footer-link" href="https://github.com/wesleyhanauer/moracarta"></a>
|
|
31
|
+
</footer>
|
|
32
|
+
|
|
33
|
+
<script type="module" src="src/scripts/main.js"></script>
|
|
34
|
+
<script type="module" src="src/scripts/templateBinding.js"></script>
|
|
31
35
|
</html>
|
package/package.json
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moracarta",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"keywords": [
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"wrangler",
|
|
14
|
-
"gift",
|
|
15
|
-
"envelope",
|
|
16
|
-
"letters",
|
|
9
|
+
"love letter",
|
|
10
|
+
"digital love letter",
|
|
11
|
+
"romantic website",
|
|
12
|
+
"love website",
|
|
17
13
|
"romantic",
|
|
18
|
-
"
|
|
14
|
+
"love",
|
|
15
|
+
"letter",
|
|
16
|
+
"valentines",
|
|
17
|
+
"valentines day",
|
|
18
|
+
"anniversary",
|
|
19
|
+
"romantic website template",
|
|
20
|
+
"website template",
|
|
21
|
+
"static website",
|
|
22
|
+
"npm",
|
|
23
|
+
"open source"
|
|
19
24
|
],
|
|
20
25
|
"files": [
|
|
21
26
|
"index.html",
|
|
@@ -23,17 +28,16 @@
|
|
|
23
28
|
"public",
|
|
24
29
|
"src/commands",
|
|
25
30
|
"src/config/fonts.js",
|
|
26
|
-
"src/
|
|
27
|
-
"src/
|
|
28
|
-
"src/i18n",
|
|
31
|
+
"src/content/letters.example.js",
|
|
32
|
+
"src/loaders/lettersLoader.js",
|
|
29
33
|
"src/scripts",
|
|
30
34
|
"src/services",
|
|
31
35
|
"src/styles",
|
|
32
36
|
"src/views"
|
|
33
37
|
],
|
|
34
|
-
"postinstall": "node src/commands/
|
|
38
|
+
"postinstall": "node src/commands/postInstall.js",
|
|
35
39
|
"bin": {
|
|
36
|
-
"moracarta": "src/
|
|
40
|
+
"moracarta": "src/config/cliConfig.js"
|
|
37
41
|
},
|
|
38
42
|
"scripts": {
|
|
39
43
|
"setup": "node src/commands/setup.js",
|
|
@@ -47,11 +51,11 @@
|
|
|
47
51
|
"type": "git",
|
|
48
52
|
"url": "git+https://github.com/WesleyHanauer/moracarta.git"
|
|
49
53
|
},
|
|
50
|
-
"author": "",
|
|
54
|
+
"author": "Wesley Hanauer",
|
|
51
55
|
"bugs": {
|
|
52
56
|
"url": "https://github.com/WesleyHanauer/moracarta/issues"
|
|
53
57
|
},
|
|
54
|
-
"homepage": "https://
|
|
58
|
+
"homepage": "https://moracarta.pages.dev",
|
|
55
59
|
"dependencies": {
|
|
56
60
|
"@inquirer/prompts": "^8.6.0",
|
|
57
61
|
"vite": "^8.2.2",
|
package/src/commands/add.js
CHANGED
|
@@ -11,7 +11,7 @@ import { getDocumentText } from "../services/googleDocs.js";
|
|
|
11
11
|
const DATA_DIRECTORY = resolve(
|
|
12
12
|
process.cwd(),
|
|
13
13
|
"src",
|
|
14
|
-
"
|
|
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/
|
|
79
|
+
"Could not find the end of the letters array in src/content/letters.js."
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
package/src/commands/deploy.js
CHANGED
|
@@ -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
|
|
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(
|
|
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.");
|
package/src/commands/remove.js
CHANGED
|
@@ -7,7 +7,7 @@ import { resolve } from "node:path";
|
|
|
7
7
|
const LETTERS_FILE = resolve(
|
|
8
8
|
process.cwd(),
|
|
9
9
|
"src",
|
|
10
|
-
"
|
|
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/
|
|
35
|
+
"Could not find the letters array in src/content/letters.js."
|
|
36
36
|
);
|
|
37
37
|
}
|
|
38
38
|
|
package/src/commands/setup.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
select,
|
|
3
|
-
input,
|
|
4
|
-
confirm
|
|
5
|
-
} from "@inquirer/prompts";
|
|
1
|
+
import { select, input, confirm } from "@inquirer/prompts";
|
|
6
2
|
import { writeFile, mkdir, cp, readFile } from "node:fs/promises";
|
|
7
3
|
import { resolve, dirname } from "node:path";
|
|
8
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -11,29 +7,18 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
11
7
|
const __dirname = dirname(__filename);
|
|
12
8
|
const PACKAGE_ROOT = resolve(__dirname, "..", "..");
|
|
13
9
|
|
|
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
10
|
const PROJECT_NAME = await input({
|
|
26
11
|
message: "What should your project be called? (Cloudflare project name, do not use 'moracarta')",
|
|
27
12
|
default: "romantic-gift",
|
|
28
13
|
});
|
|
29
14
|
|
|
30
15
|
const TITLE = await input({
|
|
31
|
-
message: "Enter the title of the application:",
|
|
16
|
+
message: "Enter the title of the application: ",
|
|
32
17
|
default: "Letters to my love"
|
|
33
18
|
});
|
|
34
19
|
|
|
35
20
|
const FONT = await select({
|
|
36
|
-
message: "What font would you like to use?",
|
|
21
|
+
message: "What font style would you like to use?",
|
|
37
22
|
choices: [
|
|
38
23
|
{ name: "Handwritten", value: "handwritten" },
|
|
39
24
|
{ name: "Classic", value: "classic" },
|
|
@@ -113,9 +98,15 @@ const DEFAULT_CLOSED_LETTER_TEXT_TOP_LINE =
|
|
|
113
98
|
const DEFAULT_CLOSED_LETTER_TEXT_BOTTOM_LINE =
|
|
114
99
|
"❤️ I love you ❤️";
|
|
115
100
|
|
|
116
|
-
const EDIT_ENVELOPE_TEXT = await
|
|
117
|
-
message: "Would you like to customize the envelope text?",
|
|
118
|
-
|
|
101
|
+
const EDIT_ENVELOPE_TEXT = await select({
|
|
102
|
+
message: "Would you like to customize the envelope text? ",
|
|
103
|
+
choices: [{
|
|
104
|
+
name: "yes",
|
|
105
|
+
value: true
|
|
106
|
+
},{
|
|
107
|
+
name: "no",
|
|
108
|
+
value: false
|
|
109
|
+
}],
|
|
119
110
|
});
|
|
120
111
|
|
|
121
112
|
let CLOSED_LETTER_TEXT_TOP_LINE = DEFAULT_CLOSED_LETTER_TEXT_TOP_LINE;
|
|
@@ -133,12 +124,21 @@ if (EDIT_ENVELOPE_TEXT) {
|
|
|
133
124
|
});
|
|
134
125
|
}
|
|
135
126
|
|
|
127
|
+
const SHOW_BRANDING = await select({
|
|
128
|
+
message: "Would you like to display 'Made with 💌 Moracarta' branding in the footer? ",
|
|
129
|
+
choices: [{
|
|
130
|
+
name: "yes",
|
|
131
|
+
value: true
|
|
132
|
+
},{
|
|
133
|
+
name: "no",
|
|
134
|
+
value: false
|
|
135
|
+
}],
|
|
136
|
+
});
|
|
137
|
+
|
|
136
138
|
const config = `/**
|
|
137
139
|
* This file was generated automatically by Moracarta setup.
|
|
138
140
|
*/
|
|
139
141
|
|
|
140
|
-
const LANGUAGE = ${JSON.stringify(LANGUAGE)};
|
|
141
|
-
|
|
142
142
|
const PROJECT_NAME = ${JSON.stringify(PROJECT_NAME)};
|
|
143
143
|
const TITLE = ${JSON.stringify(TITLE)};
|
|
144
144
|
|
|
@@ -154,8 +154,9 @@ const CLOSED_LETTER_TEXT_TOP_LINE =
|
|
|
154
154
|
const CLOSED_LETTER_TEXT_BOTTOM_LINE =
|
|
155
155
|
${JSON.stringify(CLOSED_LETTER_TEXT_BOTTOM_LINE)};
|
|
156
156
|
|
|
157
|
+
const SHOW_BRANDING = ${SHOW_BRANDING};
|
|
158
|
+
|
|
157
159
|
const globalVariables = {
|
|
158
|
-
LANGUAGE,
|
|
159
160
|
PROJECT_NAME,
|
|
160
161
|
TITLE,
|
|
161
162
|
YOUR_NAME,
|
|
@@ -163,21 +164,22 @@ const globalVariables = {
|
|
|
163
164
|
TEXT_TOP,
|
|
164
165
|
TEXT_BOTTOM,
|
|
165
166
|
CLOSED_LETTER_TEXT_TOP_LINE,
|
|
166
|
-
CLOSED_LETTER_TEXT_BOTTOM_LINE
|
|
167
|
+
CLOSED_LETTER_TEXT_BOTTOM_LINE,
|
|
168
|
+
SHOW_BRANDING
|
|
167
169
|
};
|
|
168
170
|
|
|
169
171
|
export default globalVariables;
|
|
170
172
|
`;
|
|
171
173
|
|
|
172
|
-
const
|
|
174
|
+
const dataPath = resolve(
|
|
173
175
|
process.cwd(),
|
|
174
176
|
"src",
|
|
175
177
|
"config",
|
|
176
178
|
"globalVariables.js"
|
|
177
179
|
);
|
|
178
180
|
|
|
179
|
-
await mkdir(dirname(
|
|
180
|
-
await writeFile(
|
|
181
|
+
await mkdir(dirname(dataPath), { recursive: true });
|
|
182
|
+
await writeFile(dataPath, config, "utf8");
|
|
181
183
|
|
|
182
184
|
/**
|
|
183
185
|
* Files that make up the actual site. These get copied out of the
|
|
@@ -189,9 +191,8 @@ const TEMPLATE_ENTRIES = [
|
|
|
189
191
|
"vite.config.mjs",
|
|
190
192
|
"public",
|
|
191
193
|
"src/config/fonts.js",
|
|
192
|
-
"src/
|
|
193
|
-
"src/
|
|
194
|
-
"src/i18n",
|
|
194
|
+
"src/content/letters.example.js",
|
|
195
|
+
"src/loaders/lettersLoader.js",
|
|
195
196
|
"src/scripts",
|
|
196
197
|
"src/services",
|
|
197
198
|
"src/styles",
|
|
@@ -253,5 +254,5 @@ await copyTemplateFiles();
|
|
|
253
254
|
await ensureModuleType();
|
|
254
255
|
|
|
255
256
|
console.log("\n❤️ Moracarta setup complete!");
|
|
256
|
-
console.log(`Configuration saved to: ${
|
|
257
|
+
console.log(`Configuration saved to: ${dataPath}`);
|
|
257
258
|
console.log("Run moracarta dev to test the application");
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// This file is the placeholder for the letters, this one is used until the user
|
|
2
|
+
// runs 'npx moracarta add' for the first time, then letters.js is created.
|
|
3
|
+
// This file is also usefull in the live showcase since it's built together
|
|
4
|
+
// with the rest of the app.
|
|
5
|
+
|
|
6
|
+
// Each different letter is a different object inside const letters.
|
|
7
|
+
// Color palettes are generated from the id, date and title.
|
|
8
|
+
// Since they are generated from a hashed string letters with
|
|
9
|
+
// the same information will always generate the same colors.
|
|
10
|
+
export const letters = [
|
|
11
|
+
{
|
|
12
|
+
id: 1,
|
|
13
|
+
date: 'Aug 28, 2026',
|
|
14
|
+
title: 'Custom Letter Title 1',
|
|
15
|
+
content: `My Dearest Mary Jane,
|
|
16
|
+
As I sit down to write these words, I am thinking of you and how grateful I am for everything we experience together. Life moves quickly, but whenever I pause to reflect on what brings me the most joy, you are always at the top of my mind.
|
|
17
|
+
You bring so much inspiration and comfort into my life. Whether we are sharing long conversations or simply enjoying quiet moments together, being with you feels natural and right. Your support and kindness mean more to me than I can easily put into words.
|
|
18
|
+
I wanted to remind you today of how deeply appreciated and loved you are. No matter where life takes us, know that you hold a very special place in my heart.
|
|
19
|
+
|
|
20
|
+
Yours always,
|
|
21
|
+
Peter Parker`
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 2,
|
|
25
|
+
date: 'Aug 28, 2026',
|
|
26
|
+
title: 'Custom Letter Title 2',
|
|
27
|
+
content: `Dearest Mary Jane,
|
|
28
|
+
|
|
29
|
+
I hope this letter finds you well. I wanted to take a quiet moment today to write to you and express just how much you mean to me. Even in the midst of everyday life, my thoughts frequently return to you and the wonderful moments we share.
|
|
30
|
+
Your presence brings warmth, kindness, and light into my world. Every memory with you is something I truly cherish, and I am constantly reminded of how fortunate I am to have you in my life. You have a way of making ordinary days feel special just by being yourself.
|
|
31
|
+
Thank you for your affection, your understanding, and all the happiness you bring into my life. I look forward to creating many more beautiful memories together in the time ahead.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
With all my love,
|
|
35
|
+
Peter Parker`
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 3,
|
|
39
|
+
date: 'Aug 31, 2026',
|
|
40
|
+
title: 'Custom Letter Title 3',
|
|
41
|
+
content: `Dear Mary Jane,
|
|
42
|
+
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.
|
|
43
|
+
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.
|
|
44
|
+
Love,
|
|
45
|
+
Peter Parker`
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 4,
|
|
49
|
+
date: 'Aug 31, 2026',
|
|
50
|
+
title: 'Custom Letter Title 4',
|
|
51
|
+
content: `Dear MJ,
|
|
52
|
+
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.
|
|
53
|
+
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.
|
|
54
|
+
Love,
|
|
55
|
+
Peter`
|
|
56
|
+
}
|
|
57
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// This file checks wheter or not src/content/letters.js exists, if not
|
|
2
|
+
// it loads letters.example.js as the letter provider file.
|
|
3
|
+
// It is done this way so that the user doesn't have to manually remove the
|
|
4
|
+
// placeholder example letters before adding his own letters.
|
|
5
|
+
|
|
6
|
+
// Uses meta.glob to check files with given name.
|
|
7
|
+
const modules = import.meta.glob("../content/letters*.js", {
|
|
8
|
+
eager: true,
|
|
9
|
+
import: "letters",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const letters =
|
|
13
|
+
modules["../content/letters.js"] ?? modules["../content/letters.example.js"];
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// A set of base color families used to generate a unique
|
|
2
|
+
// envelope palette for each letter based on its id, title, and date.
|
|
3
|
+
// Same code as lettersPage.js so that the same color is applied on both
|
|
4
|
+
// main page and the envelope opening animation.
|
|
6
5
|
const paletteFamilies = [
|
|
7
6
|
{ hue: 18, sat: 54, light: 88, sealHue: 345, sealSat: 76, sealLight: 42 },
|
|
8
7
|
{ hue: 26, sat: 48, light: 86, sealHue: 12, sealSat: 72, sealLight: 38 },
|
|
@@ -16,6 +15,7 @@ const paletteFamilies = [
|
|
|
16
15
|
{ hue: 14, sat: 44, light: 88, sealHue: 8, sealSat: 80, sealLight: 36 }
|
|
17
16
|
];
|
|
18
17
|
|
|
18
|
+
// Simple hash function to determine which color palette to use on the envelope.
|
|
19
19
|
function hashString(value) {
|
|
20
20
|
let hash = 0;
|
|
21
21
|
for (let index = 0; index < value.length; index += 1) {
|
|
@@ -25,18 +25,21 @@ function hashString(value) {
|
|
|
25
25
|
return Math.abs(hash);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// Keeps color value within a set range.
|
|
28
29
|
function clamp(value, min, max) {
|
|
29
30
|
return Math.min(Math.max(value, min), max);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
// Build a deterministic palette based on the letter's identity.
|
|
34
|
+
// The base hash selects one of the palette families, while the variant
|
|
35
|
+
// arrays add subtle shifts to keep similar letters visually distinct.
|
|
36
|
+
export default function generatePalette(letter) {
|
|
33
37
|
const baseHash = hashString(`${letter.id}|${letter.title}|${letter.date}`);
|
|
34
38
|
const family = paletteFamilies[baseHash % paletteFamilies.length];
|
|
35
39
|
const variant = Math.floor(baseHash / paletteFamilies.length) % 7;
|
|
36
40
|
const hueShift = [0, 14, -12, 20, -18, 26, -22][variant];
|
|
37
41
|
const satShift = [0, 8, -6, 12, -10, 14, -8][variant];
|
|
38
42
|
const lightShift = [0, -8, 8, -5, 6, -10, 10][variant];
|
|
39
|
-
|
|
40
43
|
const envelope = `hsl(${clamp(family.hue + hueShift, 0, 360)}, ${clamp(family.sat + satShift, 42, 72)}%, ${clamp(family.light + lightShift, 78, 92)}%)`;
|
|
41
44
|
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
45
|
const shadow = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 12, 30, 58)}%, ${clamp(family.light - 36 + lightShift, 42, 58)}%)`;
|
|
@@ -45,57 +48,4 @@ function generatePalette(letter) {
|
|
|
45
48
|
const text = `hsl(${family.hue}, 22%, 18%)`;
|
|
46
49
|
|
|
47
50
|
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
|
-
|
|
51
|
+
}
|
|
@@ -1,73 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
console.log('letters.script loaded');
|
|
1
|
+
import { letters } from '../loaders/lettersLoader.js';
|
|
2
|
+
import generatePalette from './generatePalette.js';
|
|
5
3
|
|
|
4
|
+
// Checks the URL to see what ID is select.
|
|
5
|
+
// Then loads letter with that same ID.
|
|
6
6
|
const params = new URLSearchParams(window.location.search);
|
|
7
7
|
const requestedId = parseInt(params.get('id'), 10);
|
|
8
8
|
const letter = letters.find((entry) => entry.id === requestedId);
|
|
9
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
10
|
function applyEnvelopePalette(letterItem) {
|
|
62
11
|
const palette = generatePalette(letterItem);
|
|
63
|
-
document.querySelector('.env-
|
|
12
|
+
document.querySelector('.env-background').style.background = palette.envelope;
|
|
64
13
|
|
|
65
|
-
const flapPolygon = document.querySelector('.env-
|
|
14
|
+
const flapPolygon = document.querySelector('.env-flap svg polygon');
|
|
66
15
|
if (flapPolygon) {
|
|
67
16
|
flapPolygon.setAttribute('fill', palette.flap);
|
|
68
17
|
}
|
|
69
18
|
|
|
70
|
-
const sidePolygons = document.querySelectorAll('.env-
|
|
19
|
+
const sidePolygons = document.querySelectorAll('.env-body-svg svg polygon');
|
|
71
20
|
if (sidePolygons.length === 3) {
|
|
72
21
|
sidePolygons[0].setAttribute('fill', palette.shadow);
|
|
73
22
|
sidePolygons[1].setAttribute('fill', palette.shadow);
|
|
@@ -99,17 +48,17 @@ if (!letter) {
|
|
|
99
48
|
const previousLetter = currentIndex > 0 ? sortedLetters[currentIndex - 1] : null;
|
|
100
49
|
const nextLetter = currentIndex < sortedLetters.length - 1 ? sortedLetters[currentIndex + 1] : null;
|
|
101
50
|
|
|
102
|
-
document.getElementById('letter-counter').textContent = `${currentIndex + 1}
|
|
51
|
+
document.getElementById('letter-counter').textContent = `${currentIndex + 1} Of ${sortedLetters.length}`;
|
|
103
52
|
|
|
104
53
|
if (previousLetter) {
|
|
105
54
|
const previousButton = document.getElementById('nav-previous');
|
|
106
|
-
previousButton.href = `./letters.
|
|
55
|
+
previousButton.href = `./letters.html?id=${previousLetter.id}`;
|
|
107
56
|
previousButton.classList.remove('invisible');
|
|
108
57
|
}
|
|
109
58
|
|
|
110
59
|
if (nextLetter) {
|
|
111
60
|
const nextButton = document.getElementById('nav-next');
|
|
112
|
-
nextButton.href = `./letters.
|
|
61
|
+
nextButton.href = `./letters.html?id=${nextLetter.id}`;
|
|
113
62
|
nextButton.classList.remove('invisible');
|
|
114
63
|
}
|
|
115
64
|
|
|
@@ -137,10 +86,10 @@ if (!letter) {
|
|
|
137
86
|
}, 2100);
|
|
138
87
|
}
|
|
139
88
|
|
|
140
|
-
function startHeartAnimation() {
|
|
141
89
|
// Floating heart particle animation for the letter view background.
|
|
142
90
|
// Particles are born after a delay, float upward, fade at the top, and die
|
|
143
91
|
// when they leave the canvas to keep the animation visually light.
|
|
92
|
+
function startHeartAnimation() {
|
|
144
93
|
const canvas = document.getElementById('canvas-hearts');
|
|
145
94
|
const context = canvas.getContext('2d');
|
|
146
95
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
@@ -252,6 +201,5 @@ function startHeartAnimation() {
|
|
|
252
201
|
context.clearRect(0, 0, width, height);
|
|
253
202
|
}
|
|
254
203
|
}
|
|
255
|
-
|
|
256
204
|
window.requestAnimationFrame(animate);
|
|
257
205
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { letters } from '../loaders/lettersLoader.js';
|
|
2
|
+
import globalVariables from '../config/globalVariables.js';
|
|
3
|
+
import generatePalette from './generatePalette.js';
|
|
4
|
+
|
|
5
|
+
const listContainer = document.getElementById('letter-list');
|
|
6
|
+
|
|
7
|
+
function renderLetters() {
|
|
8
|
+
if (!listContainer) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const orderedLetters = [...letters].reverse();
|
|
13
|
+
|
|
14
|
+
orderedLetters.forEach((letter) => {
|
|
15
|
+
const palette = generatePalette(letter);
|
|
16
|
+
const link = document.createElement('a');
|
|
17
|
+
link.href = `./src/views/letters.html?id=${letter.id}`;
|
|
18
|
+
link.className = 'envelope-link';
|
|
19
|
+
|
|
20
|
+
// Creates a new envelope object for each loaded letter.
|
|
21
|
+
link.innerHTML = `
|
|
22
|
+
<div class="envelope-wrap">
|
|
23
|
+
<div class="envelope-shadow"></div>
|
|
24
|
+
<div class="envelope" style="background:${palette.envelope}; --envelope-shadow:${palette.shadow};">
|
|
25
|
+
<div class="env-body-svg">
|
|
26
|
+
<svg viewBox="0 0 100 63" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
|
27
|
+
<polygon points="0,0 0,63 50,31" fill="${palette.shadow}" />
|
|
28
|
+
<polygon points="100,0 100,63 50,31" fill="${palette.shadow}" />
|
|
29
|
+
<polygon points="0,63 100,63 50,31" fill="${palette.base}" />
|
|
30
|
+
</svg>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="env-flap" style="border-top-color:${palette.flap};">
|
|
34
|
+
<svg viewBox="0 0 100 44" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
|
35
|
+
<polygon points="0,0 100,0 50,44" fill="${palette.flap}" />
|
|
36
|
+
</svg>
|
|
37
|
+
<div class="env-seal" style="background:${palette.seal};">♡</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="envelope-center">
|
|
41
|
+
<span class="envelope-date" style="color:${palette.text};">${letter.date}</span>
|
|
42
|
+
<span class="envelope-title" style="color:${palette.text};">${letter.title}</span>
|
|
43
|
+
<span class="envelope-hint" style="color:${palette.text};">Tap to open</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="envelope-corner tl"></div>
|
|
46
|
+
<div class="envelope-corner tr"></div>
|
|
47
|
+
<div class="envelope-corner bl"></div>
|
|
48
|
+
<div class="envelope-corner br"></div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
listContainer.appendChild(link);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
renderLetters();
|
|
58
|
+
|
|
59
|
+
if(globalVariables.SHOW_BRANDING){
|
|
60
|
+
let footer = document.getElementById('footer-link');
|
|
61
|
+
footer.textContent = "Made with 💌 Moracarta";
|
|
62
|
+
}
|
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
import globalVariables from '../config/globalVariables.js';
|
|
2
2
|
import fonts from '../config/fonts.js';
|
|
3
3
|
|
|
4
|
+
// Sets the main font for the application
|
|
4
5
|
document.documentElement.style.setProperty(
|
|
5
6
|
'--main-font',
|
|
6
7
|
fonts[globalVariables.FONT] || fonts.handwritten
|
|
7
8
|
);
|
|
8
9
|
|
|
9
|
-
//
|
|
10
|
+
// Template variables are injected from `src/config/globalVariables.js` into
|
|
10
11
|
// HTML elements using the `data-variable` attribute.
|
|
11
|
-
// HTML entities are preserved when the value contains markup characters.
|
|
12
12
|
document.querySelectorAll("[data-variable]").forEach(element => {
|
|
13
13
|
const value = globalVariables[element.dataset.variable];
|
|
14
14
|
|
|
15
|
+
//Checks if the value is to be interpreted as literal HTML or pure text
|
|
15
16
|
if (typeof value === 'string' && (value.includes('&') || value.includes('<') || value.includes('>'))) {
|
|
16
17
|
element.innerHTML = value;
|
|
17
18
|
} else {
|
|
18
19
|
element.textContent = value;
|
|
19
20
|
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
document.querySelectorAll("[data-variable-src]").forEach(element => {
|
|
23
|
-
element.src = globalVariables[element.dataset.variableSrc];
|
|
24
21
|
});
|
package/src/styles/letters.css
CHANGED
|
@@ -74,7 +74,7 @@ body {
|
|
|
74
74
|
opacity: 1;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
.env-
|
|
77
|
+
.env-background {
|
|
78
78
|
position: absolute;
|
|
79
79
|
inset: 0;
|
|
80
80
|
background: #e8d5b0;
|
|
@@ -82,7 +82,7 @@ body {
|
|
|
82
82
|
z-index: 1;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
.env-
|
|
85
|
+
.env-paper {
|
|
86
86
|
position: absolute;
|
|
87
87
|
left: 11%;
|
|
88
88
|
right: 11%;
|
|
@@ -97,7 +97,7 @@ body {
|
|
|
97
97
|
overflow: hidden;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
.env-
|
|
100
|
+
.env-paper.leaving {
|
|
101
101
|
transform: translateY(-120%);
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -111,19 +111,19 @@ body {
|
|
|
111
111
|
font-style: italic;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
.env-
|
|
114
|
+
.env-body-svg {
|
|
115
115
|
position: absolute;
|
|
116
116
|
inset: 0;
|
|
117
117
|
z-index: 3;
|
|
118
118
|
pointer-events: none;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
.env-
|
|
121
|
+
.env-body-svg svg {
|
|
122
122
|
width: 100%;
|
|
123
123
|
height: 100%;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
.env-
|
|
126
|
+
.env-flap {
|
|
127
127
|
position: absolute;
|
|
128
128
|
top: 0;
|
|
129
129
|
left: 0;
|
|
@@ -136,20 +136,20 @@ body {
|
|
|
136
136
|
overflow: visible;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
.env-
|
|
139
|
+
.env-flap.open {
|
|
140
140
|
transform: rotateX(180deg);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
.env-
|
|
143
|
+
.env-flap.behind {
|
|
144
144
|
z-index: 0;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
.env-
|
|
147
|
+
.env-flap svg {
|
|
148
148
|
width: 100%;
|
|
149
149
|
height: 100%;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
.env-
|
|
152
|
+
.env-seal {
|
|
153
153
|
position: absolute;
|
|
154
154
|
z-index: 5;
|
|
155
155
|
width: 50px;
|
|
@@ -176,7 +176,7 @@ body {
|
|
|
176
176
|
will-change: transform;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
.env-
|
|
179
|
+
.env-seal.fading {
|
|
180
180
|
opacity: 0;
|
|
181
181
|
transform: translate(-50%, -120%) scale(0.5);
|
|
182
182
|
}
|
package/src/styles/main.css
CHANGED
|
@@ -143,7 +143,7 @@ body {
|
|
|
143
143
|
transform: scale(0.97) rotate(0deg);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
.envelope-
|
|
146
|
+
.envelope-shadow {
|
|
147
147
|
position: absolute;
|
|
148
148
|
bottom: -6px;
|
|
149
149
|
left: 6px;
|
|
@@ -180,20 +180,20 @@ body {
|
|
|
180
180
|
background-repeat: no-repeat;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
.env-
|
|
183
|
+
.env-body-svg {
|
|
184
184
|
position: absolute;
|
|
185
185
|
inset: 0;
|
|
186
186
|
z-index: 1;
|
|
187
187
|
pointer-events: none;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
.env-
|
|
190
|
+
.env-body-svg svg {
|
|
191
191
|
width: 100%;
|
|
192
192
|
height: 100%;
|
|
193
193
|
display: block;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
.env-
|
|
196
|
+
.env-flap {
|
|
197
197
|
position: relative;
|
|
198
198
|
z-index: 4;
|
|
199
199
|
pointer-events: none;
|
|
@@ -202,13 +202,13 @@ body {
|
|
|
202
202
|
width: 100%;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
.env-
|
|
205
|
+
.env-flap svg {
|
|
206
206
|
width: 100%;
|
|
207
207
|
height: 100%;
|
|
208
208
|
display: block;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
.env-
|
|
211
|
+
.env-seal {
|
|
212
212
|
position: absolute;
|
|
213
213
|
z-index: 5;
|
|
214
214
|
width: 48px;
|
|
@@ -226,7 +226,7 @@ body {
|
|
|
226
226
|
bottom: -24px;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
.envelope-
|
|
229
|
+
.envelope-center {
|
|
230
230
|
position: relative;
|
|
231
231
|
z-index: 2;
|
|
232
232
|
padding: 12px 16px 16px;
|
|
@@ -235,7 +235,7 @@ body {
|
|
|
235
235
|
gap: 2px;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
.envelope-
|
|
238
|
+
.envelope-date {
|
|
239
239
|
font-size: 10px;
|
|
240
240
|
letter-spacing: 0.5px;
|
|
241
241
|
text-transform: uppercase;
|
|
@@ -244,7 +244,7 @@ body {
|
|
|
244
244
|
font-style: italic;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
.envelope-
|
|
247
|
+
.envelope-title {
|
|
248
248
|
font-family: 'Playfair Display', Georgia, serif;
|
|
249
249
|
font-size: 16px;
|
|
250
250
|
font-weight: 600;
|
|
@@ -267,7 +267,7 @@ body {
|
|
|
267
267
|
color: var(--red);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
.envelope-
|
|
270
|
+
.envelope-corner {
|
|
271
271
|
position: absolute;
|
|
272
272
|
width: 20px;
|
|
273
273
|
height: 20px;
|
|
@@ -276,25 +276,25 @@ body {
|
|
|
276
276
|
opacity: 0.5;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
.envelope-
|
|
279
|
+
.envelope-corner.tl {
|
|
280
280
|
top: 6px;
|
|
281
281
|
left: 6px;
|
|
282
282
|
border-width: 1px 0 0 1px;
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
.envelope-
|
|
285
|
+
.envelope-corner.tr {
|
|
286
286
|
top: 6px;
|
|
287
287
|
right: 6px;
|
|
288
288
|
border-width: 1px 1px 0 0;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
.envelope-
|
|
291
|
+
.envelope-corner.bl {
|
|
292
292
|
bottom: 6px;
|
|
293
293
|
left: 6px;
|
|
294
294
|
border-width: 0 0 1px 1px;
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
.envelope-
|
|
297
|
+
.envelope-corner.br {
|
|
298
298
|
bottom: 6px;
|
|
299
299
|
right: 6px;
|
|
300
300
|
border-width: 0 1px 1px 0;
|
|
@@ -310,7 +310,7 @@ body {
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
.
|
|
313
|
+
.bottom-text {
|
|
314
314
|
text-align: center;
|
|
315
315
|
margin-top: 40px;
|
|
316
316
|
padding: 0 20px;
|
|
@@ -319,14 +319,14 @@ body {
|
|
|
319
319
|
margin-right: auto;
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
.
|
|
322
|
+
.bottom-text-line {
|
|
323
323
|
width: 60px;
|
|
324
324
|
height: 1px;
|
|
325
325
|
background: linear-gradient(to right, transparent, #6b4c2a, transparent);
|
|
326
326
|
margin: 0 auto 16px;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
.
|
|
329
|
+
.bottom-text p {
|
|
330
330
|
font-size: 12px;
|
|
331
331
|
color: #6b4c2a;
|
|
332
332
|
font-style: italic;
|
|
@@ -335,4 +335,13 @@ body {
|
|
|
335
335
|
text-align: justify;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
.footer{
|
|
339
|
+
padding-top: 11vh;
|
|
340
|
+
display: flex;
|
|
341
|
+
justify-content: center;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.footer-link{
|
|
345
|
+
text-decoration: none;
|
|
346
|
+
color: rgba(176, 53, 16, 0.571);
|
|
347
|
+
}
|
|
@@ -15,24 +15,24 @@
|
|
|
15
15
|
<div class="back-link-wrap">
|
|
16
16
|
<a href="../../index.html" class="back-link">
|
|
17
17
|
<span class="back-link-arrow">←</span>
|
|
18
|
-
<span
|
|
18
|
+
<span>Return</span>
|
|
19
19
|
</a>
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
|
-
<div id="error-message" class="error" style="display:none"><p
|
|
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">
|
|
26
|
-
<div class="env-
|
|
26
|
+
<div class="env-background"></div>
|
|
27
27
|
|
|
28
|
-
<div class="env-
|
|
28
|
+
<div class="env-paper" id="paper">
|
|
29
29
|
<div class="paper-text">
|
|
30
30
|
<p data-variable="CLOSED_LETTER_TEXT_TOP_LINE"></p><br>
|
|
31
31
|
<p data-variable="CLOSED_LETTER_TEXT_BOTTOM_LINE"></p>
|
|
32
32
|
</div>
|
|
33
33
|
</div>
|
|
34
34
|
|
|
35
|
-
<div class="env-
|
|
35
|
+
<div class="env-body-svg">
|
|
36
36
|
<svg viewBox="0 0 100 63" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
|
37
37
|
<polygon points="0,0 0,63 47,42" fill="#c9a87c" />
|
|
38
38
|
<polygon points="100,0 100,63 53,42" fill="#c4a07a" />
|
|
@@ -40,13 +40,13 @@
|
|
|
40
40
|
</svg>
|
|
41
41
|
</div>
|
|
42
42
|
|
|
43
|
-
<div class="env-
|
|
43
|
+
<div class="env-flap" id="flap">
|
|
44
44
|
<svg viewBox="0 0 100 44" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
|
45
45
|
<polygon points="0,0 100,0 50,44" fill="#d4b896" />
|
|
46
46
|
</svg>
|
|
47
47
|
</div>
|
|
48
48
|
|
|
49
|
-
<div class="env-
|
|
49
|
+
<div class="env-seal" id="seal"><span class="seal-heart" aria-hidden="true">♡</span></div>
|
|
50
50
|
</div>
|
|
51
51
|
</div>
|
|
52
52
|
|
|
@@ -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="#">← <p
|
|
75
|
+
<a id="nav-previous" class="nav-button invisible" href="#">← <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
|
|
77
|
+
<a id="nav-next" class="nav-button invisible" href="#"><p>Next</p> →</a>
|
|
78
78
|
</nav>
|
|
79
|
-
|
|
80
|
-
<script type="module" src="../scripts/
|
|
81
|
-
<script type="module" src="../scripts/
|
|
82
|
-
<script type="module" src="../scripts/data.variable.js"></script>
|
|
79
|
+
|
|
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.
|
|
12
|
+
letter: resolve(import.meta.dirname, 'src/views/letters.html')
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export const letters = [
|
|
2
|
-
{
|
|
3
|
-
id: 1,
|
|
4
|
-
date: 'Aug 28, 2026',
|
|
5
|
-
title: 'Custom Letter Title 1',
|
|
6
|
-
content: `My Dearest Mary Jane,
|
|
7
|
-
As I sit down to write these words, I am thinking of you and how grateful I am for everything we experience together. Life moves quickly, but whenever I pause to reflect on what brings me the most joy, you are always at the top of my mind.
|
|
8
|
-
You bring so much inspiration and comfort into my life. Whether we are sharing long conversations or simply enjoying quiet moments together, being with you feels natural and right. Your support and kindness mean more to me than I can easily put into words.
|
|
9
|
-
I wanted to remind you today of how deeply appreciated and loved you are. No matter where life takes us, know that you hold a very special place in my heart.
|
|
10
|
-
|
|
11
|
-
Yours always,
|
|
12
|
-
Peter Parker`
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
id: 2,
|
|
16
|
-
date: 'Aug 28, 2026',
|
|
17
|
-
title: 'Custom Letter Title 2',
|
|
18
|
-
content: `Dearest Mary Jane,
|
|
19
|
-
|
|
20
|
-
I hope this letter finds you well. I wanted to take a quiet moment today to write to you and express just how much you mean to me. Even in the midst of everyday life, my thoughts frequently return to you and the wonderful moments we share.
|
|
21
|
-
Your presence brings warmth, kindness, and light into my world. Every memory with you is something I truly cherish, and I am constantly reminded of how fortunate I am to have you in my life. You have a way of making ordinary days feel special just by being yourself.
|
|
22
|
-
Thank you for your affection, your understanding, and all the happiness you bring into my life. I look forward to creating many more beautiful memories together in the time ahead.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
With all my love,
|
|
26
|
-
Peter Parker`
|
|
27
|
-
}
|
|
28
|
-
];
|
package/src/i18n/de.js
DELETED
package/src/i18n/en.js
DELETED
package/src/i18n/es.js
DELETED
package/src/i18n/fr.js
DELETED
package/src/i18n/it.js
DELETED
package/src/i18n/ja.js
DELETED
package/src/i18n/ko.js
DELETED
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
package/src/i18n/ru.js
DELETED
package/src/i18n/tl.js
DELETED
|
File without changes
|
package/src/i18n/zh.js
DELETED
package/src/scripts/i18n.js
DELETED
|
@@ -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
|