rawfeed 0.1.3 → 0.2.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 +20 -21
- data/README.md +13 -112
- data/_data/options.yml +270 -0
- data/_data/resume.yml +8 -9
- data/_includes/alert +3 -1
- data/_includes/chart +13 -32
- data/_includes/details +1 -57
- data/_includes/image +12 -4
- data/_includes/layout/blog_search.html +7 -5
- data/_includes/layout/data.liquid +21 -3
- data/_includes/layout/disqus.html +12 -26
- data/_includes/layout/footer.html +34 -16
- data/_includes/layout/giscus.html +27 -19
- data/_includes/layout/head.html +58 -19
- data/_includes/layout/header.html +127 -101
- data/_includes/layout/maintenance.html +8 -12
- data/_includes/layout/paginator.html +6 -4
- data/_includes/socials +7 -5
- data/_includes/tabs +1 -94
- data/_includes/toc +12 -152
- data/_includes/video +4 -1
- data/_layouts/blog.html +8 -7
- data/_layouts/contact.html +90 -196
- data/_layouts/default.html +42 -339
- data/_layouts/error.html +6 -4
- data/_layouts/home.html +45 -36
- data/_layouts/licenses.html +10 -0
- data/_layouts/page.html +4 -6
- data/_layouts/pixel.html +48 -0
- data/_layouts/pixels.html +71 -0
- data/_layouts/post.html +28 -31
- data/_layouts/resume.html +43 -36
- data/_layouts/tag.html +14 -3
- data/_layouts/tag_posts.html +3 -3
- data/_sass/base/_index.scss +39 -3
- data/_sass/components/_badges.scss +10 -0
- data/_sass/components/_markdown.scss +20 -17
- data/_sass/includes/_footer.scss +18 -8
- data/_sass/includes/_header.scss +24 -19
- data/_sass/includes/_highlight.scss +20 -7
- data/_sass/includes/_maintenance.scss +2 -3
- data/_sass/includes/_terminal.scss +35 -12
- data/_sass/layouts/_blog.scss +13 -9
- data/_sass/layouts/_contact.scss +6 -5
- data/_sass/layouts/_default.scss +5 -5
- data/_sass/layouts/_index.scss +3 -0
- data/_sass/layouts/_licenses.scss +7 -0
- data/_sass/layouts/_page.scss +1 -0
- data/_sass/layouts/_pixel.scss +61 -0
- data/_sass/layouts/_pixels.scss +86 -0
- data/_sass/layouts/_post.scss +4 -11
- data/_sass/layouts/_resume.scss +17 -7
- data/_sass/layouts/_tag-posts.scss +1 -2
- data/_sass/layouts/_tag.scss +12 -1
- data/_sass/main.scss +16 -1
- data/_sass/theme/_dark.scss +15 -5
- data/_sass/theme/_light.scss +9 -2
- data/assets/images/blog/.keep +0 -0
- data/assets/images/pixels/luffy.jpg +0 -0
- data/assets/js/blog.coffee +102 -0
- data/assets/js/contact.coffee +105 -0
- data/assets/js/default.coffee +172 -0
- data/assets/js/discus.coffee +30 -0
- data/assets/js/fallback/README.md +3 -0
- data/assets/js/fallback/blog.js +113 -0
- data/assets/js/fallback/contact.js +116 -0
- data/assets/js/{default.js → fallback/default.js} +50 -0
- data/assets/js/fallback/discus.js +32 -0
- data/{_includes/layout/google_analytics.html → assets/js/fallback/google_analytics.js} +7 -3
- data/assets/js/fallback/home.js +275 -0
- data/assets/js/fallback/no_inframe.js +4 -0
- data/assets/js/fallback/page.js +423 -0
- data/assets/js/fallback/pixels.js +1 -0
- data/assets/js/fallback/resume.js +13 -0
- data/assets/js/fallback/tags.js +1 -0
- data/assets/js/fallback/theme_load.js +4 -0
- data/assets/js/google_analytics.coffee +24 -0
- data/assets/js/home.coffee +250 -0
- data/assets/js/no_inframe.coffee +9 -0
- data/assets/js/page.coffee +379 -0
- data/assets/js/pixels.coffee +2 -0
- data/assets/js/resume.coffee +9 -0
- data/assets/js/tags.coffee +2 -0
- data/assets/js/theme_load.coffee +6 -0
- data/assets/json/blog_search.json +2 -2
- data/lib/rawfeed/author.rb +59 -0
- data/lib/rawfeed/csp_filters.rb +19 -0
- data/lib/rawfeed/draft.rb +1 -1
- data/lib/rawfeed/layout.rb +7 -0
- data/lib/rawfeed/page.rb +4 -2
- data/lib/rawfeed/pixel.rb +32 -0
- data/lib/rawfeed/post.rb +2 -2
- data/lib/rawfeed/resume.rb +1 -0
- data/lib/rawfeed/typescript_liquid.rb +172 -0
- data/lib/rawfeed/utils.rb +1 -0
- data/lib/rawfeed/version.rb +1 -1
- data/lib/rawfeed/with_class.rb +20 -0
- data/lib/rawfeed.rb +5 -0
- metadata +46 -12
- data/assets/js/avatar.js +0 -50
- data/assets/js/terminal.js +0 -18
- data/assets/js/toc.js +0 -22
data/_includes/toc
CHANGED
|
@@ -1,160 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
{%- include layout/data.liquid -%}
|
|
2
|
+
|
|
3
|
+
<nav id="toc"
|
|
4
|
+
style="border-top-left-radius: 0px !important; border-top-right-radius: 0px !important;"
|
|
5
|
+
class="toc{% if default_.rounding %} rounding-plugins{% endif %}{% if default_.background_focus %} background_focus{% endif %}"
|
|
6
|
+
data-toc-selector="{{ include.selector | default: '.post-content' }}"
|
|
2
7
|
data-toc-max-level="{{ include.max_level | default: 3 }}"
|
|
3
|
-
data-toc-scroll-offset="{{ include.scroll_offset | default: 20 }}"
|
|
8
|
+
data-toc-scroll-offset="{{ include.scroll_offset | default: 20 }}"
|
|
9
|
+
data-btn-show="{{ include.btn_show }}"
|
|
10
|
+
data-btn-hidden="{{ include.btn_hidden }}">
|
|
11
|
+
|
|
4
12
|
<div class="toc-head">
|
|
5
13
|
<h2>{{ include.title | default: "TOC" }}</h2>
|
|
6
|
-
<button class="toc-toggle" aria-expanded="true" type="button"
|
|
14
|
+
<button class="toc-toggle" aria-expanded="true" type="button"></button>
|
|
7
15
|
</div>
|
|
8
16
|
<div class="toc-list-wrapper">
|
|
9
17
|
<ul class="toc-list" role="list"></ul>
|
|
10
|
-
<p class="toc-empty" style="display:none">No titles found
|
|
18
|
+
<p class="toc-empty" style="display:none">No titles found. Remove TOC!</p>
|
|
11
19
|
</div>
|
|
12
20
|
</nav>
|
|
13
|
-
|
|
14
|
-
<script>
|
|
15
|
-
(function () {
|
|
16
|
-
function slugify(text) {
|
|
17
|
-
if (!text) return '';
|
|
18
|
-
return text.toString().toLowerCase().trim()
|
|
19
|
-
.normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
|
|
20
|
-
.replace(/[^\w\s-]/g, '').replace(/\s+/g, '-').replace(/--+/g, '-');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function buildTOC(tocEl) {
|
|
24
|
-
const selector = tocEl.dataset.tocSelector || '.post-content' || '.page-content';
|
|
25
|
-
const maxLevel = parseInt(tocEl.dataset.tocMaxLevel || '3', 10);
|
|
26
|
-
const offset = parseInt(tocEl.dataset.tocScrollOffset || '20', 10);
|
|
27
|
-
const root = document.querySelector(selector);
|
|
28
|
-
|
|
29
|
-
if (!root) {
|
|
30
|
-
tocEl.querySelector('.toc-empty').textContent = `Content not found (${selector})`;
|
|
31
|
-
tocEl.querySelector('.toc-empty').style.display = 'block';
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const headings = Array.from(root.querySelectorAll(Array(maxLevel).fill(0).map((_, i) => `h${i + 1}`).join(',')))
|
|
36
|
-
.filter(h => !tocEl.contains(h))
|
|
37
|
-
.filter(h => parseInt(h.tagName.substring(1)) <= maxLevel);
|
|
38
|
-
|
|
39
|
-
if (headings.length === 0) {
|
|
40
|
-
tocEl.querySelector('.toc-empty').style.display = 'block';
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const tocRoot = tocEl.querySelector('.toc-list');
|
|
45
|
-
tocRoot.innerHTML = '';
|
|
46
|
-
|
|
47
|
-
const idCounts = {};
|
|
48
|
-
headings.forEach(h => {
|
|
49
|
-
if (!h.id) {
|
|
50
|
-
let id = slugify(h.textContent);
|
|
51
|
-
if (!id) id = 'section';
|
|
52
|
-
if (idCounts[id]) { idCounts[id] += 1; id = id + '-' + idCounts[id]; }
|
|
53
|
-
else idCounts[id] = 1;
|
|
54
|
-
h.id = id;
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// Build hierarchical tree
|
|
59
|
-
const stack = [{ level: 0, ul: tocRoot }];
|
|
60
|
-
headings.forEach((h, i) => {
|
|
61
|
-
const level = parseInt(h.tagName.substring(1));
|
|
62
|
-
const li = document.createElement('li');
|
|
63
|
-
const a = document.createElement('a');
|
|
64
|
-
a.href = '#' + h.id;
|
|
65
|
-
a.textContent = h.textContent.trim();
|
|
66
|
-
a.addEventListener('click', e => {
|
|
67
|
-
e.preventDefault();
|
|
68
|
-
window.scrollTo({ top: h.getBoundingClientRect().top + window.scrollY - offset, behavior: 'smooth' });
|
|
69
|
-
history.replaceState(null, '', '#' + h.id);
|
|
70
|
-
|
|
71
|
-
// // Clicking on the TOC menu closes the TOC
|
|
72
|
-
// const wrapper = tocEl.querySelector('.toc-list-wrapper');
|
|
73
|
-
// const toggle = tocEl.querySelector('.toc-toggle');
|
|
74
|
-
// wrapper.style.display = 'none';
|
|
75
|
-
// toggle.setAttribute('aria-expanded', 'false');
|
|
76
|
-
// toggle.textContent = '{ { include.btn_show } }';
|
|
77
|
-
});
|
|
78
|
-
li.appendChild(a);
|
|
79
|
-
|
|
80
|
-
while (stack.length > 1 && level <= stack[stack.length - 1].level) stack.pop();
|
|
81
|
-
const parent = stack[stack.length - 1].ul;
|
|
82
|
-
parent.appendChild(li);
|
|
83
|
-
|
|
84
|
-
const next = headings[i + 1];
|
|
85
|
-
if (next) {
|
|
86
|
-
const nextLevel = parseInt(next.tagName.substring(1));
|
|
87
|
-
if (nextLevel > level) {
|
|
88
|
-
const newUl = document.createElement('ul');
|
|
89
|
-
li.appendChild(newUl);
|
|
90
|
-
stack.push({ level, ul: newUl });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// Active Highlight
|
|
96
|
-
const links = tocRoot.querySelectorAll('a');
|
|
97
|
-
function onScroll() {
|
|
98
|
-
const fromTop = window.scrollY + offset + 1;
|
|
99
|
-
let current = headings[0];
|
|
100
|
-
for (let i = 0; i < headings.length; i++) {
|
|
101
|
-
if (headings[i].offsetTop <= fromTop) current = headings[i];
|
|
102
|
-
}
|
|
103
|
-
links.forEach(l => l.classList.toggle('active', l.getAttribute('href') === '#' + current.id));
|
|
104
|
-
}
|
|
105
|
-
window.addEventListener('scroll', onScroll, { passive: true });
|
|
106
|
-
onScroll();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
document.addEventListener('DOMContentLoaded', function () {
|
|
110
|
-
document.querySelectorAll('.toc').forEach(toc => {
|
|
111
|
-
buildTOC(toc);
|
|
112
|
-
const toggle = toc.querySelector('.toc-toggle');
|
|
113
|
-
const wrapper = toc.querySelector('.toc-list-wrapper');
|
|
114
|
-
|
|
115
|
-
wrapper.style.display = 'none';
|
|
116
|
-
toggle.setAttribute('aria-expanded', 'false');
|
|
117
|
-
toggle.textContent = '{{ include.btn_show }}';
|
|
118
|
-
|
|
119
|
-
toggle.addEventListener('click', () => {
|
|
120
|
-
const expanded = toggle.getAttribute('aria-expanded') === 'true';
|
|
121
|
-
wrapper.style.display = expanded ? 'none' : 'block';
|
|
122
|
-
toggle.setAttribute('aria-expanded', (!expanded).toString());
|
|
123
|
-
toggle.textContent = expanded ? '{{ include.btn_show }}' : '{{ include.btn_hidden }}';
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const tocTop = toc.offsetTop;
|
|
127
|
-
|
|
128
|
-
function handleScrollFix() {
|
|
129
|
-
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
|
130
|
-
|
|
131
|
-
// Fixes pinning behavior in Chrome
|
|
132
|
-
if (scrollTop >= tocTop) {
|
|
133
|
-
toc.classList.add('fixed');
|
|
134
|
-
toc.style.position = 'fixed';
|
|
135
|
-
toc.style.top = '0';
|
|
136
|
-
toc.style.zIndex = '9999';
|
|
137
|
-
// toc.style.width = toc.offsetWidth + 'px';
|
|
138
|
-
} else {
|
|
139
|
-
toc.classList.remove('fixed');
|
|
140
|
-
toc.style.position = '';
|
|
141
|
-
toc.style.top = '';
|
|
142
|
-
toc.style.width = '';
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// // Automatically closes the TOC if it is open and scrolled to it.
|
|
146
|
-
// const rect = toc.getBoundingClientRect();
|
|
147
|
-
// if (rect.top <= 0 && toggle.getAttribute('aria-expanded') === 'true') {
|
|
148
|
-
// wrapper.style.display = 'none';
|
|
149
|
-
// toggle.setAttribute('aria-expanded', 'false');
|
|
150
|
-
// toggle.textContent = '{ { include.btn_show } }';
|
|
151
|
-
// }
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
window.addEventListener('scroll', handleScrollFix, { passive: true });
|
|
155
|
-
window.addEventListener('resize', handleScrollFix);
|
|
156
|
-
handleScrollFix();
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
})();
|
|
160
|
-
</script>
|
data/_includes/video
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
{%- include layout/data.liquid -%}
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
<div class="video-wrapper{% if default_.rounding %} rounding-plugins{% endif %}">
|
|
2
5
|
<iframe src="{{ include.url }}" title="{{ include.title | default: 'Video' }}"
|
|
3
6
|
frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen>
|
|
4
7
|
</iframe>
|
data/_layouts/blog.html
CHANGED
|
@@ -12,12 +12,15 @@ layout: default
|
|
|
12
12
|
{%- endfor -%}
|
|
13
13
|
{%- endif -%}
|
|
14
14
|
|
|
15
|
-
<div class="
|
|
15
|
+
<div id="blog" class="blog{% if default_.background_focus %} background_focus{% endif %} {% if default_.rounding %} rounding{% endif %}">
|
|
16
16
|
{%- include layout/blog_search.html -%}
|
|
17
17
|
|
|
18
18
|
<div id="posts" class="row">
|
|
19
|
+
{%- if page.description -%}
|
|
20
|
+
<div class="blog-description"><span>» </span>{{ page.description | markdownify }}</div>
|
|
21
|
+
{%- endif -%}
|
|
19
22
|
{%- if page.title -%}
|
|
20
|
-
<h2 class="blog-subtitle">[ {{
|
|
23
|
+
<h2 class="blog-subtitle">[ {{ blog_.subtitle | default: "posts" }}: {{ count_posts }} ]</h2>
|
|
21
24
|
{%- endif -%}
|
|
22
25
|
|
|
23
26
|
{%- if site.posts.size > 0 -%}
|
|
@@ -30,14 +33,12 @@ layout: default
|
|
|
30
33
|
|
|
31
34
|
{%- for post in posts -%}
|
|
32
35
|
<li class="blog-list__item">
|
|
33
|
-
{%- if
|
|
34
|
-
<span class="blog-list__meta">{% datelang post.date format:
|
|
36
|
+
{%- if datelang_ -%}
|
|
37
|
+
<span class="blog-list__meta">{% datelang post.date format:datelang_.format %}</span> »
|
|
35
38
|
{%- else -%}
|
|
36
39
|
<span class="blog-list__meta">{{ post.date | date: "%b %-d, %Y" }}</span> »
|
|
37
40
|
{%- endif -%}
|
|
38
|
-
<a class="blog-list__link" href="{{ post.url | relative_url }}">
|
|
39
|
-
{{ post.title | escape }}
|
|
40
|
-
</a>
|
|
41
|
+
<a class="blog-list__link" href="{{ post.url | relative_url }}">{{ post.title | escape }}</a>
|
|
41
42
|
{%- if post.draft -%}
|
|
42
43
|
<span class="draft-badge">[ this is a draft ]</span>
|
|
43
44
|
{%- endif -%}
|
data/_layouts/contact.html
CHANGED
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
layout: default
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
{%-
|
|
5
|
+
{%- include layout/data.liquid -%}
|
|
6
|
+
|
|
7
|
+
{%- if head_.google.recaptcha.pubkey and head_.google.apps_script.url -%}
|
|
6
8
|
|
|
7
9
|
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
|
|
8
10
|
|
|
9
|
-
<div class="
|
|
11
|
+
<div id="contact" class="contact{% if default_.background_focus %} background_focus{% endif %} {% if default_.rounding %} rounding{% endif %}">
|
|
12
|
+
|
|
10
13
|
<div class="modal fade"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
id="contactMessageModal"
|
|
15
|
+
tabindex="-1"
|
|
16
|
+
aria-labelledby="contactMessageModalLabel"
|
|
17
|
+
aria-hidden="true"
|
|
18
|
+
data-bs-backdrop="static"
|
|
19
|
+
data-bs-keyboard="false">
|
|
17
20
|
<div class="modal-dialog">
|
|
18
21
|
<div class="modal-content">
|
|
19
22
|
<div class="modal-header">
|
|
@@ -29,41 +32,39 @@ layout: default
|
|
|
29
32
|
<span class="contact-title"><strong>[ {{ page.title | downcase }} ]</strong></span>
|
|
30
33
|
<form id="contactForm" class="mt-4 contact-form">
|
|
31
34
|
<div class="mb-3">
|
|
32
|
-
{% comment %} <label for="inputName" class="form-label">{{ site.text.contact.name | default: "Name" | downcase }}</label> {% endcomment %}
|
|
33
35
|
<input id="inputName"
|
|
34
36
|
name="name"
|
|
35
37
|
type="text"
|
|
36
|
-
placeholder="{{
|
|
38
|
+
placeholder="{{ contact_.name.placeholder | default: 'First and last name' }}"
|
|
37
39
|
class="form-control contact-form__name"
|
|
38
40
|
required>
|
|
39
41
|
</div>
|
|
40
42
|
<div class="mb-3">
|
|
41
|
-
{% comment %} <label for="inputEmail" class="form-label">{{ site.text.contact.name | default: "Email address" | downcase }}</label> {% endcomment %}
|
|
42
43
|
<input id="inputEmail"
|
|
43
44
|
name="email"
|
|
44
45
|
type="email"
|
|
45
|
-
placeholder="{{
|
|
46
|
+
placeholder="{{ contact_.email.placeholder | default: 'Your best email address' }}"
|
|
46
47
|
class="form-control contact-form__email"
|
|
47
48
|
aria-describedby="emailHelp"
|
|
48
49
|
required>
|
|
49
50
|
<div id="emailHelp" class="form-text contact-form__help">
|
|
50
|
-
{{
|
|
51
|
+
{{ contact_.email.help | default: "We'll never share your email with anyone else." }}
|
|
51
52
|
</div>
|
|
52
53
|
</div>
|
|
53
54
|
<textarea id="textMessage"
|
|
54
55
|
name="message"
|
|
55
56
|
class="form-control contact-form__message"
|
|
56
|
-
placeholder="{{
|
|
57
|
+
placeholder="{{ contact_.message.placeholder | default: 'Write your message here' }}"
|
|
57
58
|
style="min-height: 150px"
|
|
58
59
|
required></textarea>
|
|
59
|
-
<small style="display: block; font-size: 8pt; opacity: .6">{{
|
|
60
|
-
<!-- TODO: version: 0.
|
|
61
|
-
<div id="g-recaptcha" class="g-recaptcha mt-2" data-sitekey="{{
|
|
60
|
+
<small style="display: block; font-size: 8pt; opacity: .6">{{ contact_.message.caracters.warning.content }}</small>
|
|
61
|
+
<!-- TODO: version: 0.3.0 Make reCaptcha change themes instantly -->
|
|
62
|
+
<div id="g-recaptcha" class="g-recaptcha mt-2" data-sitekey="{{ head_.google.recaptcha.pubkey }}" style="display: inline-block; margin: 5px 0;"></div>
|
|
62
63
|
<div class="d-flex justify-content-end mb-5">
|
|
63
64
|
<button id="submitButton"
|
|
64
65
|
type="submit"
|
|
65
66
|
class="btn contact-form__submit">
|
|
66
|
-
{{
|
|
67
|
+
{{ contact_.button.text | default: 'Send!' }}
|
|
67
68
|
</button>
|
|
68
69
|
</div>
|
|
69
70
|
</form>
|
|
@@ -73,119 +74,9 @@ layout: default
|
|
|
73
74
|
</div>
|
|
74
75
|
</div>
|
|
75
76
|
|
|
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
77
|
{%- else -%}
|
|
187
78
|
|
|
188
|
-
<div class="
|
|
79
|
+
<div class="contact-disabled{% if default_.background_focus %} background_focus{% endif %} {% if default_.rounding %} rounding{% endif %}">
|
|
189
80
|
<h1 style="background-color: yellow;color: black;padding: 10px">Warning: Email form disabled</h1>
|
|
190
81
|
<p>To use the email submission form, you need to:</p>
|
|
191
82
|
<p>1 - Copy the script below and implement it in <a href="https://script.google.com" target="_blank">Google Apps Script</a>:</p>
|
|
@@ -194,92 +85,95 @@ layout: default
|
|
|
194
85
|
<p>Note2: Without editing the script in Google Apps Script, you need to deploy it again.</p>
|
|
195
86
|
</blockquote>
|
|
196
87
|
|
|
197
|
-
{% highlight javascript linenos %}
|
|
198
|
-
// IMPORTANT! You must put your gmail email here.
|
|
199
|
-
const TO_ADDRESS = "YOUR EMAIL GMAIL";
|
|
88
|
+
{% highlight javascript linenos %}
|
|
89
|
+
// IMPORTANT! You must put your gmail email here.
|
|
90
|
+
const TO_ADDRESS = "YOUR EMAIL GMAIL";
|
|
200
91
|
|
|
201
|
-
// Get the secret key from the Script Properties
|
|
202
|
-
const RECAPTCHA_SECRET_KEY = PropertiesService.getScriptProperties().getProperty('RECAPTCHA_SECRET_KEY');
|
|
92
|
+
// Get the secret key from the Script Properties
|
|
93
|
+
const RECAPTCHA_SECRET_KEY = PropertiesService.getScriptProperties().getProperty('RECAPTCHA_SECRET_KEY');
|
|
203
94
|
|
|
204
|
-
// Function to validate the reCAPTCHA token
|
|
205
|
-
function validateRecaptcha(token) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
95
|
+
// Function to validate the reCAPTCHA token
|
|
96
|
+
function validateRecaptcha(token) {
|
|
97
|
+
if (!token) {
|
|
98
|
+
throw new Error("Missing reCAPTCHA token.");
|
|
99
|
+
}
|
|
100
|
+
const url = "https://www.google.com/recaptcha/api/siteverify";
|
|
101
|
+
const payload = {
|
|
102
|
+
secret: RECAPTCHA_SECRET_KEY,
|
|
103
|
+
response: token
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const response = UrlFetchApp.fetch(url, {
|
|
107
|
+
method: "post",
|
|
108
|
+
payload: payload
|
|
109
|
+
});
|
|
214
110
|
|
|
215
|
-
|
|
216
|
-
method: "post",
|
|
217
|
-
payload: payload
|
|
218
|
-
});
|
|
111
|
+
const result = JSON.parse(response.getContentText());
|
|
219
112
|
|
|
220
|
-
|
|
113
|
+
if (!result.success) {
|
|
114
|
+
throw new Error("reCAPTCHA verification failed: " + (result['error-codes'] || 'Unknown error.'));
|
|
115
|
+
}
|
|
221
116
|
|
|
222
|
-
|
|
223
|
-
throw new Error("reCAPTCHA verification failed: " + (result['error-codes'] || 'Unknown error.'));
|
|
117
|
+
return true;
|
|
224
118
|
}
|
|
225
119
|
|
|
226
|
-
return true;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
function doPost(e) {
|
|
231
|
-
try {
|
|
232
|
-
const data = JSON.parse(e.postData.contents);
|
|
233
120
|
|
|
234
|
-
|
|
235
|
-
|
|
121
|
+
function doPost(e) {
|
|
122
|
+
try {
|
|
123
|
+
const data = JSON.parse(e.postData.contents);
|
|
236
124
|
|
|
237
|
-
|
|
238
|
-
|
|
125
|
+
// 1. Validate the reCAPTCHA token first!
|
|
126
|
+
validateRecaptcha(data['g-recaptcha-response']);
|
|
239
127
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
128
|
+
// 2. If validation passed, continue with the rest of the code
|
|
129
|
+
const { name, email, message } = data;
|
|
243
130
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
`;
|
|
131
|
+
if (!name || !email || !message) {
|
|
132
|
+
throw new Error("Missing form data.");
|
|
133
|
+
}
|
|
252
134
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
135
|
+
const subject = "New website message " + name;
|
|
136
|
+
const htmlBody = `
|
|
137
|
+
<p>You have received a new message from your website.:</p><hr>
|
|
138
|
+
<p><b>Name:</b> ${name}</p>
|
|
139
|
+
<p><b>Email:</b> <a href="mailto:${email}">${email}</a></p>
|
|
140
|
+
<p><b>Message:</b></p>
|
|
141
|
+
<p style="white-space: pre-wrap;">${message}</p><hr>
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
MailApp.sendEmail({
|
|
145
|
+
to: TO_ADDRESS,
|
|
146
|
+
subject: subject,
|
|
147
|
+
htmlBody: htmlBody,
|
|
148
|
+
replyTo: email
|
|
149
|
+
});
|
|
259
150
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
151
|
+
return ContentService
|
|
152
|
+
.createTextOutput(JSON.stringify({ 'result': 'success', 'message': 'Message sent!' }))
|
|
153
|
+
.setMimeType(ContentService.MimeType.JSON);
|
|
263
154
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
155
|
+
} catch (err) {
|
|
156
|
+
Logger.log(err.toString());
|
|
157
|
+
return ContentService
|
|
158
|
+
.createTextOutput(JSON.stringify({ 'result': 'error', 'message': err.toString() }))
|
|
159
|
+
.setMimeType(ContentService.MimeType.JSON);
|
|
160
|
+
}
|
|
269
161
|
}
|
|
270
|
-
}
|
|
271
|
-
|
|
162
|
+
{% endhighlight %}
|
|
163
|
+
|
|
272
164
|
<p>2 - Create a <a href="https://console.cloud.google.com/security/recaptcha" target="_blank">reCaptcha</a>
|
|
273
165
|
on Google and add the reCaptcha <strong>PRIVATE</strong> key to the <strong>Google Apps Script</strong> script property.</p>
|
|
274
166
|
<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>
|
|
167
|
+
|
|
275
168
|
{% highlight yml linenos %}
|
|
276
|
-
google:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
169
|
+
google:
|
|
170
|
+
###
|
|
171
|
+
###
|
|
172
|
+
apps_script:
|
|
173
|
+
url: "https://script.google.com/macros/s/BuD..."
|
|
174
|
+
recaptcha:
|
|
175
|
+
pubkey: "8Lci194rAAAAA70Sv..."
|
|
283
176
|
{% endhighlight %}
|
|
177
|
+
|
|
284
178
|
</div>
|
|
285
179
|
{%- endif -%}
|