rawfeed 0.0.1 → 0.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.
Files changed (134) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +21 -0
  3. data/README.md +153 -1
  4. data/_data/resume.yml +184 -0
  5. data/_includes/alert +3 -0
  6. data/_includes/chart +34 -0
  7. data/_includes/details +57 -0
  8. data/_includes/enddetails +2 -0
  9. data/_includes/endtabs +2 -0
  10. data/_includes/image +61 -0
  11. data/_includes/layout/blog_search.html +18 -0
  12. data/_includes/layout/data.liquid +3 -0
  13. data/_includes/layout/disqus.html +30 -0
  14. data/_includes/layout/footer.html +34 -0
  15. data/_includes/layout/giscus.html +21 -0
  16. data/_includes/layout/google_analytics.html +11 -0
  17. data/_includes/layout/head.html +59 -0
  18. data/_includes/layout/header.html +143 -0
  19. data/_includes/layout/maintenance.html +30 -0
  20. data/_includes/layout/paginator.html +35 -0
  21. data/_includes/socials +22 -0
  22. data/_includes/tabs +94 -0
  23. data/_includes/toc +160 -0
  24. data/_includes/video +10 -0
  25. data/_layouts/blog.html +46 -0
  26. data/_layouts/contact.html +285 -0
  27. data/_layouts/default.html +248 -0
  28. data/_layouts/error.html +15 -0
  29. data/_layouts/home.html +58 -0
  30. data/_layouts/page.html +9 -0
  31. data/_layouts/post.html +103 -0
  32. data/_layouts/resume.html +260 -0
  33. data/_layouts/tag.html +22 -0
  34. data/_layouts/tag_posts.html +27 -0
  35. data/_sass/base/_index.scss +63 -0
  36. data/_sass/base/_reset.scss +10 -0
  37. data/_sass/base/_typography.scss +0 -0
  38. data/_sass/components/_badges.scss +24 -0
  39. data/_sass/components/_button.scss +17 -0
  40. data/_sass/components/_forms.scss +42 -0
  41. data/_sass/components/_gifs.scss +5 -0
  42. data/_sass/components/_index.scss +5 -0
  43. data/_sass/components/_markdown.scss +453 -0
  44. data/_sass/includes/_footer.scss +45 -0
  45. data/_sass/includes/_header.scss +240 -0
  46. data/_sass/includes/_highlight.scss +87 -0
  47. data/_sass/includes/_index.scss +9 -0
  48. data/_sass/includes/_maintenance.scss +16 -0
  49. data/_sass/includes/_paginator.scss +22 -0
  50. data/_sass/includes/_rouge-dark.scss +81 -0
  51. data/_sass/includes/_rouge-light.scss +121 -0
  52. data/_sass/includes/_terminal.scss +208 -0
  53. data/_sass/layouts/_blog.scss +96 -0
  54. data/_sass/layouts/_contact.scss +55 -0
  55. data/_sass/layouts/_default.scss +14 -0
  56. data/_sass/layouts/_error.scss +18 -0
  57. data/_sass/layouts/_home.scss +19 -0
  58. data/_sass/layouts/_index.scss +10 -0
  59. data/_sass/layouts/_page.scss +5 -0
  60. data/_sass/layouts/_post.scss +109 -0
  61. data/_sass/layouts/_resume.scss +330 -0
  62. data/_sass/layouts/_tag-posts.scss +48 -0
  63. data/_sass/layouts/_tag.scss +22 -0
  64. data/_sass/main.scss +128 -0
  65. data/_sass/theme/_dark.scss +79 -0
  66. data/_sass/theme/_index.scss +13 -0
  67. data/_sass/theme/_light.scss +56 -0
  68. data/assets/css/style.scss +5 -0
  69. data/assets/images/avatar_back.png +0 -0
  70. data/assets/images/avatar_dark.png +0 -0
  71. data/assets/images/avatar_light.png +0 -0
  72. data/assets/images/favicon.png +0 -0
  73. data/assets/js/avatar.js +50 -0
  74. data/assets/js/blog_search.js +102 -0
  75. data/assets/js/default.js +148 -0
  76. data/assets/js/terminal.js +15 -0
  77. data/assets/js/toc.js +20 -0
  78. data/assets/json/blog_search.json +16 -0
  79. data/assets/vendor/bootstrap/css/bootstrap-grid.css +4124 -0
  80. data/assets/vendor/bootstrap/css/bootstrap-grid.css.map +1 -0
  81. data/assets/vendor/bootstrap/css/bootstrap-grid.min.css +7 -0
  82. data/assets/vendor/bootstrap/css/bootstrap-grid.min.css.map +1 -0
  83. data/assets/vendor/bootstrap/css/bootstrap-grid.rtl.css +4123 -0
  84. data/assets/vendor/bootstrap/css/bootstrap-grid.rtl.css.map +1 -0
  85. data/assets/vendor/bootstrap/css/bootstrap-grid.rtl.min.css +7 -0
  86. data/assets/vendor/bootstrap/css/bootstrap-grid.rtl.min.css.map +1 -0
  87. data/assets/vendor/bootstrap/css/bootstrap-reboot.css +488 -0
  88. data/assets/vendor/bootstrap/css/bootstrap-reboot.css.map +1 -0
  89. data/assets/vendor/bootstrap/css/bootstrap-reboot.min.css +7 -0
  90. data/assets/vendor/bootstrap/css/bootstrap-reboot.min.css.map +1 -0
  91. data/assets/vendor/bootstrap/css/bootstrap-reboot.rtl.css +485 -0
  92. data/assets/vendor/bootstrap/css/bootstrap-reboot.rtl.css.map +1 -0
  93. data/assets/vendor/bootstrap/css/bootstrap-reboot.rtl.min.css +7 -0
  94. data/assets/vendor/bootstrap/css/bootstrap-reboot.rtl.min.css.map +1 -0
  95. data/assets/vendor/bootstrap/css/bootstrap-utilities.css +4266 -0
  96. data/assets/vendor/bootstrap/css/bootstrap-utilities.css.map +1 -0
  97. data/assets/vendor/bootstrap/css/bootstrap-utilities.min.css +7 -0
  98. data/assets/vendor/bootstrap/css/bootstrap-utilities.min.css.map +1 -0
  99. data/assets/vendor/bootstrap/css/bootstrap-utilities.rtl.css +4257 -0
  100. data/assets/vendor/bootstrap/css/bootstrap-utilities.rtl.css.map +1 -0
  101. data/assets/vendor/bootstrap/css/bootstrap-utilities.rtl.min.css +7 -0
  102. data/assets/vendor/bootstrap/css/bootstrap-utilities.rtl.min.css.map +1 -0
  103. data/assets/vendor/bootstrap/css/bootstrap.css +10878 -0
  104. data/assets/vendor/bootstrap/css/bootstrap.css.map +1 -0
  105. data/assets/vendor/bootstrap/css/bootstrap.min.css +7 -0
  106. data/assets/vendor/bootstrap/css/bootstrap.min.css.map +1 -0
  107. data/assets/vendor/bootstrap/css/bootstrap.rtl.css +10842 -0
  108. data/assets/vendor/bootstrap/css/bootstrap.rtl.css.map +1 -0
  109. data/assets/vendor/bootstrap/css/bootstrap.rtl.min.css +7 -0
  110. data/assets/vendor/bootstrap/css/bootstrap.rtl.min.css.map +1 -0
  111. data/assets/vendor/bootstrap/js/bootstrap.bundle.js +7075 -0
  112. data/assets/vendor/bootstrap/js/bootstrap.bundle.js.map +1 -0
  113. data/assets/vendor/bootstrap/js/bootstrap.bundle.min.js +7 -0
  114. data/assets/vendor/bootstrap/js/bootstrap.bundle.min.js.map +1 -0
  115. data/assets/vendor/bootstrap/js/bootstrap.esm.js +5202 -0
  116. data/assets/vendor/bootstrap/js/bootstrap.esm.js.map +1 -0
  117. data/assets/vendor/bootstrap/js/bootstrap.esm.min.js +7 -0
  118. data/assets/vendor/bootstrap/js/bootstrap.esm.min.js.map +1 -0
  119. data/assets/vendor/bootstrap/js/bootstrap.js +5249 -0
  120. data/assets/vendor/bootstrap/js/bootstrap.js.map +1 -0
  121. data/assets/vendor/bootstrap/js/bootstrap.min.js +7 -0
  122. data/assets/vendor/bootstrap/js/bootstrap.min.js.map +1 -0
  123. data/assets/vendor/simple-jekyll-search.js +433 -0
  124. data/assets/vendor/simple-jekyll-search.min.js +6 -0
  125. data/lib/rawfeed/draft.rb +31 -0
  126. data/lib/rawfeed/installer.rb +37 -0
  127. data/lib/rawfeed/layout.rb +138 -0
  128. data/lib/rawfeed/page.rb +33 -0
  129. data/lib/rawfeed/post.rb +60 -0
  130. data/lib/rawfeed/resume.rb +59 -0
  131. data/lib/rawfeed/utils.rb +74 -0
  132. data/lib/rawfeed/version.rb +1 -1
  133. data/lib/rawfeed.rb +5 -7
  134. metadata +145 -2
@@ -0,0 +1,285 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+
5
+ {%- if site.google.recaptcha.pubkey and site.google.apps_script.url -%}
6
+
7
+ <script src="https://www.google.com/recaptcha/api.js" async defer></script>
8
+
9
+ <div class="container contact">
10
+ <div class="modal fade"
11
+ id="contactMessageModal"
12
+ tabindex="-1"
13
+ aria-labelledby="contactMessageModalLabel"
14
+ aria-hidden="true"
15
+ data-bs-backdrop="static"
16
+ data-bs-keyboard="false">
17
+ <div class="modal-dialog">
18
+ <div class="modal-content">
19
+ <div class="modal-header">
20
+ <h5 class="modal-title" id="contactMessageModalLabel"></h5>
21
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
22
+ </div>
23
+ <div class="modal-body"></div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+ <div class="row">
29
+ <span class="contact-title"><strong>[&nbsp;{{ page.title | downcase }}&nbsp;]</strong></span>
30
+ <form id="contactForm" class="mt-4 contact-form">
31
+ <div class="mb-3">
32
+ {% comment %} <label for="inputName" class="form-label">{{ site.text.contact.name | default: "Name" | downcase }}</label> {% endcomment %}
33
+ <input id="inputName"
34
+ name="name"
35
+ type="text"
36
+ placeholder="{{ site.text.contact.name.placeholder | default: 'First and last name' }}"
37
+ class="form-control contact-form__name"
38
+ required>
39
+ </div>
40
+ <div class="mb-3">
41
+ {% comment %} <label for="inputEmail" class="form-label">{{ site.text.contact.name | default: "Email address" | downcase }}</label> {% endcomment %}
42
+ <input id="inputEmail"
43
+ name="email"
44
+ type="email"
45
+ placeholder="{{ site.text.contact.email.placeholder | default: 'Your best email address' }}"
46
+ class="form-control contact-form__email"
47
+ aria-describedby="emailHelp"
48
+ required>
49
+ <div id="emailHelp" class="form-text contact-form__help">
50
+ {{ site.text.contact.email.help | default: "We'll never share your email with anyone else." }}
51
+ </div>
52
+ </div>
53
+ <textarea id="textMessage"
54
+ name="message"
55
+ class="form-control contact-form__message"
56
+ placeholder="{{ site.text.contact.message.placeholder | default: 'Write your message here' }}"
57
+ style="min-height: 150px"
58
+ required></textarea>
59
+ <small style="display: block; font-size: 8pt; opacity: .6">{{ site.text.contact.message.caracters.warning.content }}</small>
60
+ <!-- TODO: version: 0.2.0 Make reCaptcha change themes instantly -->
61
+ <div id="g-recaptcha" class="g-recaptcha mt-2" data-sitekey="{{ site.google.recaptcha.pubkey }}" style="display: inline-block; margin: 5px 0;"></div>
62
+ <div class="d-flex justify-content-end mb-5">
63
+ <button id="submitButton"
64
+ type="submit"
65
+ class="btn contact-form__submit">
66
+ {{ site.text.contact.button.text | default: 'Send!' }}
67
+ </button>
68
+ </div>
69
+ </form>
70
+ </div>
71
+ <div class="row">
72
+ <div class="contact-content">{{ content }}</div>
73
+ </div>
74
+ </div>
75
+
76
+ <script>
77
+ const form = document.getElementById("contactForm");
78
+ const submitButton = document.getElementById("submitButton");
79
+ const endpoint = "{{ site.google.apps_script.url }}"; // URL Google Apps Script
80
+
81
+ // get modal
82
+ function showModal(title, message, type = 'success') {
83
+ const modalEl = document.getElementById('contactMessageModal');
84
+ const modalTitle = modalEl.querySelector('.modal-title');
85
+ const modalBody = modalEl.querySelector('.modal-body');
86
+ const modalContent = modalEl.querySelector('.modal-content');
87
+
88
+ modalContent.classList.remove('contact-message-success', 'contact-message-error', 'contact-message-warning');
89
+
90
+
91
+ // Apply the color according to the type
92
+ if (type === 'success') {
93
+ modalContent.classList.add('contact-message-success');
94
+ } else if (type === 'error') {
95
+ modalContent.classList.add('contact-message-error');
96
+ } else if (type === 'warning') {
97
+ modalContent.classList.add('contact-message-warning');
98
+ }
99
+
100
+ modalTitle.innerHTML = title;
101
+ modalBody.innerHTML = message;
102
+
103
+ const bsModal = new bootstrap.Modal(modalEl);
104
+ bsModal.show();
105
+ }
106
+
107
+ form.addEventListener("submit", async (e) => {
108
+ e.preventDefault();
109
+
110
+ const recaptchaResponse = grecaptcha.getResponse();
111
+ if (!recaptchaResponse) {
112
+ showModal(
113
+ "{{ site.text.contact.recaptcha.warning.title | default: 'Warning' }}",
114
+ "{{ site.text.contact.recaptcha.warning.content | default: "Please tick the 'I'm not a robot' box." }}",
115
+ "warning"
116
+ );
117
+ return;
118
+ }
119
+
120
+ const textarea = document.getElementById('textMessage');
121
+ const text = textarea.value.trim();
122
+ if (text.length < {{ site.text.contact.message.caracters.min }}) {
123
+ showModal(
124
+ "{{ site.text.contact.message.caracters.warning.title | default: 'Warning' }}",
125
+ "{{ site.text.contact.message.caracters.warning.content | default: "The message must have at least 50 characters." }}",
126
+ "warning"
127
+ );
128
+ return;
129
+ }
130
+
131
+ submitButton.disabled = true;
132
+ submitButton.textContent = "{{ site.text.contact.message.status | default: "Sending...Wait" }}";
133
+
134
+ const formData = new FormData(form);
135
+ const data = Object.fromEntries(formData.entries());
136
+
137
+ try {
138
+ const response = await fetch(endpoint, {
139
+ method: "POST",
140
+ redirect: "follow",
141
+ body: JSON.stringify(data)
142
+ });
143
+
144
+ const result = await response.json();
145
+
146
+ if (result.result === 'success') {
147
+ form.reset();
148
+ grecaptcha.reset();
149
+ showModal(
150
+ "{{ site.text.contact.message.success.title | default: 'Message Sent' }}",
151
+ "{{ site.text.contact.message.success.content | default: 'Your message has been sent successfully!' }}",
152
+ "success"
153
+ );
154
+ } else {
155
+ showModal(
156
+ "{{ site.text.contact.message.error.title | default: 'Error' }}",
157
+ "{{ site.text.contact.message.error.content | default: 'Something went wrong while sending your message.' }}",
158
+ "error"
159
+ );
160
+ throw new Error(result.message || "{{ site.text.contact.message.error.content | default: "An unknown error has occurred." }}");
161
+ }
162
+
163
+ } catch (error) {
164
+ console.error("Error sending:", error);
165
+ if (error.message.includes("reCAPTCHA")) {
166
+ showModal(
167
+ "{{ site.text.contact.message.error.title | default: 'Error' }}",
168
+ "{{ site.text.contact.recaptcha.fail | default: "Verification failed. Please reload the page and try again." }}",
169
+ "error"
170
+ );
171
+ } else {
172
+ showModal(
173
+ "{{ site.text.contact.message.error.title | default: 'Error' }}",
174
+ "{{ site.text.contact.recaptcha.error | default: "An error occurred while sending the message. Please try again." }}",
175
+ "error"
176
+ );
177
+ }
178
+ grecaptcha.reset();
179
+
180
+ } finally {
181
+ submitButton.disabled = false;
182
+ submitButton.textContent = "{{ site.text.contact.button.text | default: "Send!" }}";
183
+ }
184
+ });
185
+ </script>
186
+ {%- else -%}
187
+
188
+ <div class="contat-disabled">
189
+ <h1 style="background-color: yellow;color: black;padding: 10px">Warning: Email form disabled</h1>
190
+ <p>To use the email submission form, you need to:</p>
191
+ <p>1 - Copy the script below and implement it in <a href="https://script.google.com" target="_blank">Google Apps Script</a>:</p>
192
+ <blockquote>
193
+ <p>Note1: Don't forget to put your gmail in the script.</p>
194
+ <p>Note2: Without editing the script in Google Apps Script, you need to deploy it again.</p>
195
+ </blockquote>
196
+
197
+ {% highlight javascript linenos %}
198
+ // IMPORTANT! You must put your gmail email here.
199
+ const TO_ADDRESS = "YOUR EMAIL GMAIL";
200
+
201
+ // Get the secret key from the Script Properties
202
+ const RECAPTCHA_SECRET_KEY = PropertiesService.getScriptProperties().getProperty('RECAPTCHA_SECRET_KEY');
203
+
204
+ // Function to validate the reCAPTCHA token
205
+ function validateRecaptcha(token) {
206
+ if (!token) {
207
+ throw new Error("Missing reCAPTCHA token.");
208
+ }
209
+ const url = "https://www.google.com/recaptcha/api/siteverify";
210
+ const payload = {
211
+ secret: RECAPTCHA_SECRET_KEY,
212
+ response: token
213
+ };
214
+
215
+ const response = UrlFetchApp.fetch(url, {
216
+ method: "post",
217
+ payload: payload
218
+ });
219
+
220
+ const result = JSON.parse(response.getContentText());
221
+
222
+ if (!result.success) {
223
+ throw new Error("reCAPTCHA verification failed: " + (result['error-codes'] || 'Unknown error.'));
224
+ }
225
+
226
+ return true;
227
+ }
228
+
229
+
230
+ function doPost(e) {
231
+ try {
232
+ const data = JSON.parse(e.postData.contents);
233
+
234
+ // 1. Validate the reCAPTCHA token first!
235
+ validateRecaptcha(data['g-recaptcha-response']);
236
+
237
+ // 2. If validation passed, continue with the rest of the code
238
+ const { name, email, message } = data;
239
+
240
+ if (!name || !email || !message) {
241
+ throw new Error("Missing form data.");
242
+ }
243
+
244
+ const subject = "New website message " + name;
245
+ const htmlBody = `
246
+ <p>You have received a new message from your website.:</p><hr>
247
+ <p><b>Name:</b> ${name}</p>
248
+ <p><b>Email:</b> <a href="mailto:${email}">${email}</a></p>
249
+ <p><b>Message:</b></p>
250
+ <p style="white-space: pre-wrap;">${message}</p><hr>
251
+ `;
252
+
253
+ MailApp.sendEmail({
254
+ to: TO_ADDRESS,
255
+ subject: subject,
256
+ htmlBody: htmlBody,
257
+ replyTo: email
258
+ });
259
+
260
+ return ContentService
261
+ .createTextOutput(JSON.stringify({ 'result': 'success', 'message': 'Message sent!' }))
262
+ .setMimeType(ContentService.MimeType.JSON);
263
+
264
+ } catch (err) {
265
+ Logger.log(err.toString());
266
+ return ContentService
267
+ .createTextOutput(JSON.stringify({ 'result': 'error', 'message': err.toString() }))
268
+ .setMimeType(ContentService.MimeType.JSON);
269
+ }
270
+ }
271
+ {% endhighlight %}
272
+ <p>2 - Create a <a href="https://console.cloud.google.com/security/recaptcha" target="_blank">reCaptcha</a>
273
+ on Google and add the reCaptcha <strong>PRIVATE</strong> key to the <strong>Google Apps Script</strong> script property.</p>
274
+ <p>3 - Copy the reCaptcha <strong>PUBLIC</strong> key and the <strong>Google Apps Script</strong> URL and place them in <strong>_config.yml:</strong></p>
275
+ {% highlight yml linenos %}
276
+ google:
277
+ ###
278
+ ###
279
+ apps_script:
280
+ url: "https://script.google.com/macros/s/BuD..."
281
+ recaptcha:
282
+ pubkey: "8Lci194rAAAAA70Sv..."
283
+ {% endhighlight %}
284
+ </div>
285
+ {%- endif -%}
@@ -0,0 +1,248 @@
1
+ <!-- Theme for Jekyll.rb by: © William C. Canin -->
2
+ {%- if site.maintenance.enable -%}
3
+ {%- include layout/maintenance.html -%}
4
+ {%- else -%}
5
+
6
+ {%- assign index = site.pages | where: "path", "index.md" | first -%}
7
+
8
+ <!DOCTYPE html>
9
+ <html id="top" lang="{{ site.lang | default: 'en-US' }}" data-theme="light">
10
+ {%- include layout/head.html -%}
11
+ <body data-layout="{{ page.layout | default: '' }}" data-terminal-enabled="{{ site.home.terminal.enable | default: false }}">
12
+ {%- if site.home.terminal.enable and page.url == "/" and index.layout == "home" -%}
13
+ <div class="default default-terminal" style="max-width: {{ site.layout.width | default: '780px' }} !important;">
14
+ {%- include layout/header.html -%}
15
+ </div>
16
+ <main class="content">{{ content }}</main>
17
+ {%- include layout/footer.html -%}
18
+ {%- else -%}
19
+ <div class="default" style="max-width: {{ site.layout.width | default: '780px' }} !important;">
20
+ {%- include layout/header.html -%}
21
+ <main class="content">{{ content }}</main>
22
+
23
+ {%- include layout/footer.html -%}
24
+ </div>
25
+ {%- endif -%}
26
+ </body>
27
+
28
+ <!-- Scripts -->
29
+ <script src="{{ '/assets/vendor/bootstrap/js/bootstrap.bundle.js' | relative_url }}"></script>
30
+
31
+ {%- if page.comments != false and site.blog.post.comments.provider == 'giscus' -%}
32
+ <script>
33
+ window.giscusThemes = {
34
+ light: "{{ site.blog.post.comments.giscus.theme_light | default: 'light' }}",
35
+ dark: "{{ site.blog.post.comments.giscus.theme_dark | default: 'dark' }}"
36
+ };
37
+ </script>
38
+ {%- endif -%}
39
+
40
+ <script src="{{ '/assets/js/default.js' | relative_url }}"></script>
41
+
42
+ {%- if page.url == '/' and site.home.terminal.enable -%}
43
+ <script src="{{ '/assets/js/terminal.js' | relative_url }}"></script>
44
+ {%- endif -%}
45
+
46
+ {%- if site.avatar.open -%}
47
+ <script src="{{ '/assets/js/avatar.js' | relative_url }}"></script>
48
+ {%- endif -%}
49
+
50
+ {%- if site.blog.search.enable -%}
51
+ {%- if page.url == '/blog/' or page.url == '/blog/index.html' -%}
52
+ <script src="{{ '/assets/vendor/simple-jekyll-search.min.js' | relative_url }}"></script>
53
+ <script src="{{ '/assets/js/blog_search.js' | relative_url }}"></script>
54
+ <script>
55
+ var sjs = SimpleJekyllSearch({
56
+ searchInput: document.getElementById('blog-search__input'),
57
+ resultsContainer: document.getElementById('blog-search__results'),
58
+ searchResultTemplate: '<li><span class="blog-list__meta"><time datetime="{date}">{date}</time></span>&nbsp;»&nbsp; <a class="blog-list__link" href="{{ site.url }}{url}">{title}</a></li>',
59
+ noResultsText: '<p>{{ site.text.blog.no_results | default: "No results found" }}</p>',
60
+ json: '/assets/json/blog_search.json'
61
+ })
62
+ </script>
63
+ {%- endif -%}
64
+ {%- endif -%}
65
+
66
+ {%- if site.home.terminal.enable and page.url == "/" -%}
67
+ <script>
68
+ document.addEventListener("DOMContentLoaded", () => {
69
+ const screen = document.getElementById('screen');
70
+ const terminal = document.getElementById("terminal");
71
+ const socialsEl = document.getElementById("terminal-screen--socials");
72
+
73
+ const commands = {
74
+ help: `{{ site.text.home.terminal.commands }}`,
75
+ about: document.getElementById("home-content").innerHTML,
76
+ socials: socialsEl ? socialsEl.innerHTML : `{{ site.text.home.terminal.no_socials }}`,
77
+ };
78
+
79
+ function createInputLine() {
80
+ const line = document.createElement('div');
81
+ line.className = 'line';
82
+
83
+ const prompt = document.createElement('span');
84
+ prompt.className = 'prompt';
85
+ prompt.textContent = `[{{ site.text.home.terminal.user }}@{{ site.text.home.terminal.hostname }}:~]$`;
86
+
87
+ // wrapper para conter input, cursor e measure
88
+ const wrapper = document.createElement('span');
89
+ wrapper.className = 'input-wrapper';
90
+
91
+ const input = document.createElement('input');
92
+ input.type = 'text';
93
+ input.className = 'input';
94
+ input.placeholder = `{{ site.text.home.terminal.welcome }}`;
95
+ input.spellcheck = false;
96
+ input.autocomplete = 'off';
97
+ input.autocorrect = 'off';
98
+ input.autocapitalize = 'off';
99
+
100
+ const cursor = document.createElement('span');
101
+ cursor.className = 'cursor';
102
+
103
+ const measure = document.createElement('span');
104
+ measure.className = 'measure';
105
+
106
+ wrapper.appendChild(input);
107
+ wrapper.appendChild(cursor);
108
+ wrapper.appendChild(measure);
109
+
110
+ line.appendChild(prompt);
111
+ line.appendChild(wrapper);
112
+ screen.appendChild(line);
113
+
114
+ input.focus();
115
+ screen.scrollTop = screen.scrollHeight;
116
+
117
+ // Updates the fake cursor position based on the input's selectionStart
118
+ function updateCursor() {
119
+ const sel = input.selectionStart || 0;
120
+ // measure the text to the position of the caret
121
+ measure.textContent = input.value.slice(0, sel);
122
+ const textWidth = measure.offsetWidth; // largura do texto sem scroll
123
+ const visibleLeft = textWidth - input.scrollLeft;
124
+ cursor.style.left = visibleLeft + 'px';
125
+
126
+ // ensure the caret is visible (for long texts): adjust input's scrollLeft
127
+ const paddingRight = 10;
128
+ if (textWidth - input.scrollLeft > input.clientWidth - paddingRight) {
129
+ input.scrollLeft = textWidth - input.clientWidth + paddingRight;
130
+ cursor.style.left = (textWidth - input.scrollLeft) + 'px';
131
+ } else if (textWidth < input.scrollLeft) {
132
+ input.scrollLeft = textWidth;
133
+ cursor.style.left = (textWidth - input.scrollLeft) + 'px';
134
+ }
135
+ }
136
+
137
+ // show/hide cursor animation as focus changes
138
+ function onFocus() { cursor.style.opacity = '1'; updateCursor(); }
139
+ function onBlur() { cursor.style.opacity = '0'; }
140
+
141
+ input.addEventListener('input', updateCursor);
142
+ input.addEventListener('keydown', (e) => {
143
+ // Update position on keys that do not trigger input immediately (arrows, delete, etc.)
144
+ setTimeout(updateCursor, 0);
145
+
146
+ if (e.key === 'Enter') {
147
+ e.preventDefault();
148
+ const cmd = input.value.trim().toLowerCase();
149
+ if (cmd) {
150
+ // remove input/cursor/measure and place fixed text
151
+ wrapper.removeChild(input);
152
+ wrapper.removeChild(cursor);
153
+ wrapper.removeChild(measure);
154
+ const cmdText = document.createElement('span');
155
+ cmdText.textContent = cmd;
156
+ wrapper.appendChild(cmdText);
157
+ processCommand(cmd);
158
+ } else {
159
+ // if you enter without command, it just creates a new empty line (with prompt)
160
+ wrapper.removeChild(input);
161
+ wrapper.removeChild(cursor);
162
+ wrapper.removeChild(measure);
163
+ const blank = document.createElement('span');
164
+ blank.textContent = '';
165
+ wrapper.appendChild(blank);
166
+ }
167
+ // New linr input
168
+ createInputLine();
169
+ } else if (e.key === 'Escape') {
170
+ e.preventDefault();
171
+ screen.innerHTML = '';
172
+ createInputLine();
173
+ }
174
+ });
175
+
176
+ // arrows, mouse click, mouseup (position caret), etc.
177
+ input.addEventListener('keyup', updateCursor);
178
+ input.addEventListener('click', () => {
179
+ // updates after click (selectionStart already set)
180
+ setTimeout(updateCursor, 0);
181
+ });
182
+ input.addEventListener('mouseup', () => setTimeout(updateCursor, 0));
183
+ input.addEventListener('focus', onFocus);
184
+ input.addEventListener('blur', onBlur);
185
+
186
+ updateCursor();
187
+ }
188
+
189
+ // processes commands
190
+ function processCommand(cmd) {
191
+ switch(true){
192
+ case cmd === 'help': commandsPrint(commands.help, mode='html'); break;
193
+ case cmd === 'date': commandsPrint(new Date().toString(), mode='text'); break;
194
+ // case cmd.startsWith('echo '): commandsPrint(cmd.split(' ').slice(1).join(' ')); break;
195
+ case cmd === 'about': writeLineHTML(commands.about); break;
196
+ case cmd === 'socials': writeLineHTML(commands.socials); break;
197
+ case cmd === 'clear': screen.innerHTML=''; break;
198
+ default: if(cmd) commandsPrint(cmd + `{{ site.text.home.terminal.error }}`, mode='text');
199
+ }
200
+ }
201
+
202
+ function writeLineHTML(content, mode = 'html') {
203
+ const wrapper = document.createElement('div');
204
+ wrapper.className = 'line-wrapper';
205
+
206
+ if (mode === 'html') {
207
+ wrapper.innerHTML = content;
208
+ } else {
209
+ wrapper.textContent = content;
210
+ }
211
+ screen.appendChild(wrapper);
212
+ screen.scrollTop = screen.scrollHeight;
213
+ }
214
+
215
+ function commandsPrint(text, mode = 'html') {
216
+ // creates the wrapper to group all the lined
217
+ const wrapper = document.createElement('div');
218
+ wrapper.className = 'line-wrapper';
219
+
220
+ text.split('\n').forEach((t) => {
221
+ const line = document.createElement('div');
222
+ line.className = 'line';
223
+ if (mode === 'html') {
224
+ line.innerHTML = t;
225
+ } else {
226
+ line.textContent = t;
227
+ }
228
+ wrapper.appendChild(line);
229
+ });
230
+
231
+ screen.appendChild(wrapper);
232
+ screen.scrollTop = screen.scrollHeight;
233
+ }
234
+
235
+ // start terminal
236
+ createInputLine();
237
+
238
+ // when clicking on the terminal, it always focuses on the last existing input
239
+ terminal.addEventListener("click", (e) => {
240
+ // avoids focusing when clicking a header button, etc.
241
+ const lastInput = screen.querySelector('.input:last-of-type');
242
+ if (lastInput) lastInput.focus();
243
+ });
244
+ });
245
+ </script>
246
+ {%- endif -%}
247
+ </html>
248
+ {%- endif -%}
@@ -0,0 +1,15 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+
5
+ <div class="container error">
6
+ <div class="row">
7
+ <div class="col-sm text-center">
8
+ {%- if site.text.error.image -%}
9
+ <img class="error-image" src="{{ site.text.error.image }}" alt="error 404">
10
+ {%- endif -%}
11
+ <h1 class="error-title">{{ site.text.error.title | escape }}</h1>
12
+ <h2 class="error-description">{{ site.text.error.message | markdownify }}</h2>
13
+ </div>
14
+ </div>
15
+ </div>
@@ -0,0 +1,58 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+
5
+ {%- if site.home.terminal.enable and page.url == "/" -%}
6
+ <div class="d-flex justify-content-center align-items-center mb-5 home home-terminal">
7
+ <div id="terminal" class="terminal">
8
+ <div class="terminal-header">
9
+ <div class="terminal-header__btn terminal-header__close" title="close"></div>
10
+ <div class="terminal-header__btn terminal-header__min" title="minimize"></div>
11
+ <div class="terminal-header__btn terminal-header__max" title="maximize"></div>
12
+ <div class="terminal-header__title">{{ site.text.home.terminal.user }}@{{ site.text.home.terminal.hostname }}</div>
13
+ </div>
14
+
15
+ <div id="screen" class="terminal-screen">
16
+ <div id="home-content" style="display:none !important;">
17
+ {{ content }}
18
+ </div>
19
+
20
+ <div id="terminal-screen--socials" style="display:none">
21
+ {% assign links = site.socials.links %}<strong>.&nbsp;</strong>
22
+ {% for item in links %}
23
+ <a class="socials-link" title="{{ item.title }}" href="{{ item.url }}" target="_blank">
24
+ {{ item.title }}
25
+ </a><strong>&nbsp;.&nbsp;</strong>
26
+ {% endfor %}
27
+ </div>
28
+
29
+ </div>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="home-text">
34
+ <div class="container home">
35
+ <div class="row">
36
+ <div class="content" style="{%- if page.text_center -%} text-align: center; {%- endif -%}">
37
+ {{ content }}
38
+ </div>
39
+ </div>
40
+ </div>
41
+ {%- include socials pos="center" -%}
42
+ </div>
43
+
44
+ {%- else -%}
45
+ <div class="container home">
46
+ <div class="row">
47
+ <div class="content" style="{%- if page.text_center -%} text-align: center; {%- endif -%}">
48
+ {{ content }}
49
+ </div>
50
+ </div>
51
+ </div>
52
+ {%- endif -%}
53
+
54
+ {%- if site.home.terminal.enable == false -%}
55
+ {%- if page.url == "/" and site.socials.enable -%}
56
+ {%- include socials pos="center" -%}
57
+ {%- endif -%}
58
+ {%- endif -%}
@@ -0,0 +1,9 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+
5
+ <div class="container page">
6
+ <div class="row">
7
+ <div class="page-content">{{ content }}</div>
8
+ </div>
9
+ </div>