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.
- checksums.yaml +4 -4
- data/LICENSE.txt +21 -0
- data/README.md +153 -1
- data/_data/resume.yml +184 -0
- data/_includes/alert +3 -0
- data/_includes/chart +34 -0
- data/_includes/details +57 -0
- data/_includes/enddetails +2 -0
- data/_includes/endtabs +2 -0
- data/_includes/image +61 -0
- data/_includes/layout/blog_search.html +18 -0
- data/_includes/layout/data.liquid +3 -0
- data/_includes/layout/disqus.html +30 -0
- data/_includes/layout/footer.html +34 -0
- data/_includes/layout/giscus.html +21 -0
- data/_includes/layout/google_analytics.html +11 -0
- data/_includes/layout/head.html +59 -0
- data/_includes/layout/header.html +143 -0
- data/_includes/layout/maintenance.html +30 -0
- data/_includes/layout/paginator.html +35 -0
- data/_includes/socials +22 -0
- data/_includes/tabs +94 -0
- data/_includes/toc +160 -0
- data/_includes/video +10 -0
- data/_layouts/blog.html +46 -0
- data/_layouts/contact.html +285 -0
- data/_layouts/default.html +248 -0
- data/_layouts/error.html +15 -0
- data/_layouts/home.html +58 -0
- data/_layouts/page.html +9 -0
- data/_layouts/post.html +103 -0
- data/_layouts/resume.html +260 -0
- data/_layouts/tag.html +22 -0
- data/_layouts/tag_posts.html +27 -0
- data/_sass/base/_index.scss +63 -0
- data/_sass/base/_reset.scss +10 -0
- data/_sass/base/_typography.scss +0 -0
- data/_sass/components/_badges.scss +24 -0
- data/_sass/components/_button.scss +17 -0
- data/_sass/components/_forms.scss +42 -0
- data/_sass/components/_gifs.scss +5 -0
- data/_sass/components/_index.scss +5 -0
- data/_sass/components/_markdown.scss +453 -0
- data/_sass/includes/_footer.scss +45 -0
- data/_sass/includes/_header.scss +240 -0
- data/_sass/includes/_highlight.scss +87 -0
- data/_sass/includes/_index.scss +9 -0
- data/_sass/includes/_maintenance.scss +16 -0
- data/_sass/includes/_paginator.scss +22 -0
- data/_sass/includes/_rouge-dark.scss +81 -0
- data/_sass/includes/_rouge-light.scss +121 -0
- data/_sass/includes/_terminal.scss +208 -0
- data/_sass/layouts/_blog.scss +96 -0
- data/_sass/layouts/_contact.scss +55 -0
- data/_sass/layouts/_default.scss +14 -0
- data/_sass/layouts/_error.scss +18 -0
- data/_sass/layouts/_home.scss +19 -0
- data/_sass/layouts/_index.scss +10 -0
- data/_sass/layouts/_page.scss +5 -0
- data/_sass/layouts/_post.scss +109 -0
- data/_sass/layouts/_resume.scss +330 -0
- data/_sass/layouts/_tag-posts.scss +48 -0
- data/_sass/layouts/_tag.scss +22 -0
- data/_sass/main.scss +128 -0
- data/_sass/theme/_dark.scss +79 -0
- data/_sass/theme/_index.scss +13 -0
- data/_sass/theme/_light.scss +56 -0
- data/assets/css/style.scss +5 -0
- data/assets/images/avatar_back.png +0 -0
- data/assets/images/avatar_dark.png +0 -0
- data/assets/images/avatar_light.png +0 -0
- data/assets/images/favicon.png +0 -0
- data/assets/js/avatar.js +50 -0
- data/assets/js/blog_search.js +102 -0
- data/assets/js/default.js +148 -0
- data/assets/js/terminal.js +15 -0
- data/assets/js/toc.js +20 -0
- data/assets/json/blog_search.json +16 -0
- data/assets/vendor/bootstrap/css/bootstrap-grid.css +4124 -0
- data/assets/vendor/bootstrap/css/bootstrap-grid.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap-grid.min.css +7 -0
- data/assets/vendor/bootstrap/css/bootstrap-grid.min.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap-grid.rtl.css +4123 -0
- data/assets/vendor/bootstrap/css/bootstrap-grid.rtl.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap-grid.rtl.min.css +7 -0
- data/assets/vendor/bootstrap/css/bootstrap-grid.rtl.min.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap-reboot.css +488 -0
- data/assets/vendor/bootstrap/css/bootstrap-reboot.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap-reboot.min.css +7 -0
- data/assets/vendor/bootstrap/css/bootstrap-reboot.min.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap-reboot.rtl.css +485 -0
- data/assets/vendor/bootstrap/css/bootstrap-reboot.rtl.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap-reboot.rtl.min.css +7 -0
- data/assets/vendor/bootstrap/css/bootstrap-reboot.rtl.min.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap-utilities.css +4266 -0
- data/assets/vendor/bootstrap/css/bootstrap-utilities.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap-utilities.min.css +7 -0
- data/assets/vendor/bootstrap/css/bootstrap-utilities.min.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap-utilities.rtl.css +4257 -0
- data/assets/vendor/bootstrap/css/bootstrap-utilities.rtl.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap-utilities.rtl.min.css +7 -0
- data/assets/vendor/bootstrap/css/bootstrap-utilities.rtl.min.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap.css +10878 -0
- data/assets/vendor/bootstrap/css/bootstrap.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap.min.css +7 -0
- data/assets/vendor/bootstrap/css/bootstrap.min.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap.rtl.css +10842 -0
- data/assets/vendor/bootstrap/css/bootstrap.rtl.css.map +1 -0
- data/assets/vendor/bootstrap/css/bootstrap.rtl.min.css +7 -0
- data/assets/vendor/bootstrap/css/bootstrap.rtl.min.css.map +1 -0
- data/assets/vendor/bootstrap/js/bootstrap.bundle.js +7075 -0
- data/assets/vendor/bootstrap/js/bootstrap.bundle.js.map +1 -0
- data/assets/vendor/bootstrap/js/bootstrap.bundle.min.js +7 -0
- data/assets/vendor/bootstrap/js/bootstrap.bundle.min.js.map +1 -0
- data/assets/vendor/bootstrap/js/bootstrap.esm.js +5202 -0
- data/assets/vendor/bootstrap/js/bootstrap.esm.js.map +1 -0
- data/assets/vendor/bootstrap/js/bootstrap.esm.min.js +7 -0
- data/assets/vendor/bootstrap/js/bootstrap.esm.min.js.map +1 -0
- data/assets/vendor/bootstrap/js/bootstrap.js +5249 -0
- data/assets/vendor/bootstrap/js/bootstrap.js.map +1 -0
- data/assets/vendor/bootstrap/js/bootstrap.min.js +7 -0
- data/assets/vendor/bootstrap/js/bootstrap.min.js.map +1 -0
- data/assets/vendor/simple-jekyll-search.js +433 -0
- data/assets/vendor/simple-jekyll-search.min.js +6 -0
- data/lib/rawfeed/draft.rb +31 -0
- data/lib/rawfeed/installer.rb +37 -0
- data/lib/rawfeed/layout.rb +138 -0
- data/lib/rawfeed/page.rb +33 -0
- data/lib/rawfeed/post.rb +60 -0
- data/lib/rawfeed/resume.rb +59 -0
- data/lib/rawfeed/utils.rb +74 -0
- data/lib/rawfeed/version.rb +1 -1
- data/lib/rawfeed.rb +5 -7
- 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>[ {{ page.title | downcase }} ]</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> » <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 -%}
|
data/_layouts/error.html
ADDED
|
@@ -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>
|
data/_layouts/home.html
ADDED
|
@@ -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>. </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> . </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 -%}
|