rawfeed 0.1.4 → 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 +12 -130
- data/_data/options.yml +270 -0
- data/_data/resume.yml +8 -8
- 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 +6 -4
- data/_includes/layout/data.liquid +21 -3
- data/_includes/layout/disqus.html +12 -26
- data/_includes/layout/footer.html +33 -17
- data/_includes/layout/giscus.html +27 -19
- data/_includes/layout/head.html +41 -41
- data/_includes/layout/header.html +127 -101
- data/_includes/layout/maintenance.html +6 -10
- data/_includes/layout/paginator.html +6 -4
- data/_includes/socials +7 -5
- data/_includes/tabs +1 -94
- data/_includes/toc +11 -194
- data/_includes/video +4 -1
- data/_layouts/blog.html +8 -7
- data/_layouts/contact.html +90 -196
- data/_layouts/default.html +42 -341
- data/_layouts/error.html +6 -4
- data/_layouts/home.html +45 -36
- data/_layouts/licenses.html +10 -0
- data/_layouts/page.html +4 -4
- data/_layouts/pixel.html +48 -0
- data/_layouts/pixels.html +71 -1
- data/_layouts/post.html +28 -29
- data/_layouts/resume.html +41 -34
- 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 +8 -5
- data/_sass/includes/_footer.scss +5 -2
- data/_sass/includes/_header.scss +23 -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 +16 -3
- 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 +8 -1
- data/_sass/theme/_light.scss +8 -1
- 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/{_includes/layout/capture_scripts.liquid → assets/js/fallback/theme_load.js} +0 -2
- 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 +3 -0
- data/lib/rawfeed/draft.rb +1 -1
- data/lib/rawfeed/layout.rb +7 -0
- data/lib/rawfeed/page.rb +2 -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 -1
- metadata +44 -12
- data/assets/js/avatar.js +0 -59
- data/assets/js/terminal.js +0 -18
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
{%- include layout/data.liquid -%}
|
|
5
|
+
|
|
6
|
+
document.addEventListener "DOMContentLoaded", ->
|
|
7
|
+
|
|
8
|
+
contact = document.getElementById "contact"
|
|
9
|
+
|
|
10
|
+
if contact
|
|
11
|
+
form = document.getElementById "contactForm"
|
|
12
|
+
submitButton = document.getElementById "submitButton"
|
|
13
|
+
endpoint = "{{ head_.google.apps_script.url }}" # URL Google Apps Script
|
|
14
|
+
|
|
15
|
+
# função para exibir modal
|
|
16
|
+
showModal = (title, message, type = 'success') ->
|
|
17
|
+
modalEl = document.getElementById 'contactMessageModal'
|
|
18
|
+
modalTitle = modalEl.querySelector '.modal-title'
|
|
19
|
+
modalBody = modalEl.querySelector '.modal-body'
|
|
20
|
+
modalContent = modalEl.querySelector '.modal-content'
|
|
21
|
+
|
|
22
|
+
modalContent.classList.remove 'contact-message-success', 'contact-message-error', 'contact-message-warning'
|
|
23
|
+
|
|
24
|
+
# Aplica a cor de acordo com o tipo
|
|
25
|
+
if type is 'success'
|
|
26
|
+
modalContent.classList.add 'contact-message-success'
|
|
27
|
+
else if type is 'error'
|
|
28
|
+
modalContent.classList.add 'contact-message-error'
|
|
29
|
+
else if type is 'warning'
|
|
30
|
+
modalContent.classList.add 'contact-message-warning'
|
|
31
|
+
|
|
32
|
+
modalTitle.innerHTML = title
|
|
33
|
+
modalBody.innerHTML = message
|
|
34
|
+
|
|
35
|
+
bsModal = new bootstrap.Modal modalEl
|
|
36
|
+
bsModal.show()
|
|
37
|
+
|
|
38
|
+
form.addEventListener "submit", (e) ->
|
|
39
|
+
e.preventDefault()
|
|
40
|
+
|
|
41
|
+
recaptchaResponse = grecaptcha.getResponse()
|
|
42
|
+
unless recaptchaResponse
|
|
43
|
+
showModal(
|
|
44
|
+
"""{{ contact_.recaptcha.warning.title | default: "Warning" }}""",
|
|
45
|
+
"""{{ contact_.recaptcha.warning.content | default: "Please tick the 'I'm not a robot' box." }}""",
|
|
46
|
+
"warning"
|
|
47
|
+
)
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
textarea = document.getElementById 'textMessage'
|
|
51
|
+
text = textarea.value.trim()
|
|
52
|
+
if text.length < "{{ contact_.message.caracters.min }}"
|
|
53
|
+
showModal(
|
|
54
|
+
"""{{ contact_.message.caracters.warning.title | default: "Warning" }}""",
|
|
55
|
+
"""{{ contact_.message.caracters.warning.content | default: "The message must have at least 50 characters." }}""",
|
|
56
|
+
"warning"
|
|
57
|
+
)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
submitButton.disabled = true
|
|
61
|
+
submitButton.textContent = """{{ contact_.message.status | default: "Sending...Wait" }}"""
|
|
62
|
+
|
|
63
|
+
formData = new FormData form
|
|
64
|
+
data = Object.fromEntries formData.entries()
|
|
65
|
+
|
|
66
|
+
fetch(endpoint,
|
|
67
|
+
method: "POST"
|
|
68
|
+
redirect: "follow"
|
|
69
|
+
body: JSON.stringify data
|
|
70
|
+
).then (response) ->
|
|
71
|
+
response.json()
|
|
72
|
+
.then (result) ->
|
|
73
|
+
if result.result is 'success'
|
|
74
|
+
form.reset()
|
|
75
|
+
grecaptcha.reset()
|
|
76
|
+
showModal(
|
|
77
|
+
"""{{ contact_.message.success.title | default: "Message Sent" }}""",
|
|
78
|
+
"""{{ contact_.message.success.content | default: "Your message has been sent successfully!" }}""",
|
|
79
|
+
"success"
|
|
80
|
+
)
|
|
81
|
+
else
|
|
82
|
+
showModal(
|
|
83
|
+
"""{{ contact_.message.error.title | default: "Error" }}""",
|
|
84
|
+
"""{{ contact_.message.error.content | default: "Something went wrong while sending your message." }}""",
|
|
85
|
+
"error"
|
|
86
|
+
)
|
|
87
|
+
throw new Error result.message or "{{ contact_.message.error.content | default: 'An unknown error has occurred.' }}"
|
|
88
|
+
.catch (error) ->
|
|
89
|
+
console.error "Error sending:", error
|
|
90
|
+
if error.message.includes "reCAPTCHA"
|
|
91
|
+
showModal(
|
|
92
|
+
"""{{ contact_.message.error.title | default: "Error" }}""",
|
|
93
|
+
"""{{ contact_.recaptcha.fail | default: "Verification failed. Please reload the page and try again." }}""",
|
|
94
|
+
"error"
|
|
95
|
+
)
|
|
96
|
+
else
|
|
97
|
+
showModal(
|
|
98
|
+
"""{{ contact_.message.error.title | default: "Error" }}""",
|
|
99
|
+
"""{{ contact_.recaptcha.error | default: "An error occurred while sending the message. Please try again." }}""",
|
|
100
|
+
"error"
|
|
101
|
+
)
|
|
102
|
+
grecaptcha.reset()
|
|
103
|
+
.finally ->
|
|
104
|
+
submitButton.disabled = false
|
|
105
|
+
submitButton.textContent = """{{ contact_.button.text | default: "Send!" }}"""
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
{%- include layout/data.liquid -%}
|
|
5
|
+
|
|
6
|
+
# Script that will be used throughout the site
|
|
7
|
+
|
|
8
|
+
document.addEventListener "DOMContentLoaded", ->
|
|
9
|
+
# lock menu context (click right mouse)
|
|
10
|
+
# ------------------------------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
document.addEventListener 'contextmenu', (e) -> e.preventDefault()
|
|
13
|
+
|
|
14
|
+
# avatar
|
|
15
|
+
#-------------------------------------------------------------------------------------------------
|
|
16
|
+
do ->
|
|
17
|
+
flipperAvatars = document.querySelectorAll('.avatar-flipper__open-true')
|
|
18
|
+
modalEl = document.getElementById('avatarModal')
|
|
19
|
+
modalAvatar = document.getElementById('modalAvatar')
|
|
20
|
+
header = document.querySelector('.header')
|
|
21
|
+
bsModal = new bootstrap.Modal(modalEl)
|
|
22
|
+
|
|
23
|
+
for flipper in flipperAvatars
|
|
24
|
+
do (flipper) -> # the 'do' creates a new scope and passes the 'flipper' as an argument
|
|
25
|
+
flipper.addEventListener "click", ->
|
|
26
|
+
# NOW, 'flipper' refers to the correct avatar as it was isolated by IIFE
|
|
27
|
+
card = flipper.querySelector('.avatar-card')
|
|
28
|
+
backImage = flipper.querySelector('.avatar-back img')
|
|
29
|
+
backImageSrc = backImage.src
|
|
30
|
+
|
|
31
|
+
card.classList.add "flip-avatar"
|
|
32
|
+
|
|
33
|
+
card.addEventListener "animationend", (event) ->
|
|
34
|
+
card.classList.remove "flip-avatar"
|
|
35
|
+
modalAvatar.src = backImageSrc
|
|
36
|
+
bsModal.show()
|
|
37
|
+
, { once: true }
|
|
38
|
+
|
|
39
|
+
modalEl.addEventListener "shown.bs.modal", ->
|
|
40
|
+
modalAvatar.classList.remove "modal-avatar"
|
|
41
|
+
modalAvatar.offsetWidth
|
|
42
|
+
modalAvatar.classList.add "modal-avatar"
|
|
43
|
+
header.classList.remove "modal-active"
|
|
44
|
+
|
|
45
|
+
for f in flipperAvatars
|
|
46
|
+
f.classList.add "hidden"
|
|
47
|
+
|
|
48
|
+
modalEl.addEventListener "hidden.bs.modal", ->
|
|
49
|
+
for f in flipperAvatars
|
|
50
|
+
f.classList.remove "hidden"
|
|
51
|
+
|
|
52
|
+
# change theme light/dark
|
|
53
|
+
# ------------------------------------------------------------------------------------------------
|
|
54
|
+
#
|
|
55
|
+
toggleButton = document.getElementById 'toggle-theme'
|
|
56
|
+
iconToggleButton = toggleButton.querySelector 'i'
|
|
57
|
+
root = document.documentElement
|
|
58
|
+
|
|
59
|
+
setTheme = (theme) ->
|
|
60
|
+
root.setAttribute 'data-theme', theme
|
|
61
|
+
|
|
62
|
+
iconToggleButton.classList.remove 'fa-sun', 'fa-moon'
|
|
63
|
+
iconToggleButton.classList.add if theme is 'dark' then 'fa-sun' else 'fa-moon'
|
|
64
|
+
|
|
65
|
+
if typeof setGiscusTheme is 'function'
|
|
66
|
+
setGiscusTheme theme
|
|
67
|
+
|
|
68
|
+
localStorage.setItem 'theme', theme
|
|
69
|
+
|
|
70
|
+
# boot with saved or light
|
|
71
|
+
setTheme localStorage.getItem('theme') or 'light'
|
|
72
|
+
|
|
73
|
+
# change theme on click
|
|
74
|
+
toggleButton.addEventListener 'click', ->
|
|
75
|
+
current = root.getAttribute 'data-theme'
|
|
76
|
+
next = if current is 'light' then 'dark' else 'light'
|
|
77
|
+
setTheme next
|
|
78
|
+
|
|
79
|
+
# Show/disappear top button
|
|
80
|
+
# ------------------------------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
topButton = document.getElementById "top-link"
|
|
83
|
+
scrollThreshold = 700
|
|
84
|
+
window.onscroll = -> scrollFunction()
|
|
85
|
+
|
|
86
|
+
scrollFunction = ->
|
|
87
|
+
if document.body.scrollTop > scrollThreshold or document.documentElement.scrollTop > scrollThreshold
|
|
88
|
+
topButton.style.display = "block"
|
|
89
|
+
else
|
|
90
|
+
topButton.style.display = "none"
|
|
91
|
+
|
|
92
|
+
topButton.addEventListener "click", -> topFunction()
|
|
93
|
+
|
|
94
|
+
topFunction = ->
|
|
95
|
+
window.scrollTo
|
|
96
|
+
top: 0
|
|
97
|
+
behavior: 'smooth'
|
|
98
|
+
|
|
99
|
+
# function Giscus
|
|
100
|
+
# ------------------------------------------------------------------------------------------------
|
|
101
|
+
#
|
|
102
|
+
setGiscusTheme = (theme) ->
|
|
103
|
+
# The function only executes if the themes object exists
|
|
104
|
+
if window.giscusThemes
|
|
105
|
+
giscusTheme = if theme is 'light' then window.giscusThemes.light else window.giscusThemes.dark
|
|
106
|
+
|
|
107
|
+
message =
|
|
108
|
+
giscus:
|
|
109
|
+
setConfig:
|
|
110
|
+
theme: giscusTheme
|
|
111
|
+
|
|
112
|
+
# Let's use a timeout to ensure the Giscus iframe is ready
|
|
113
|
+
giscusInterval = setInterval ->
|
|
114
|
+
giscusFrame = document.querySelector 'iframe.giscus-frame'
|
|
115
|
+
# Se o iframe existir no documento...
|
|
116
|
+
if giscusFrame
|
|
117
|
+
# ...we sent the message...
|
|
118
|
+
giscusFrame.contentWindow.postMessage message, 'https://giscus.app'
|
|
119
|
+
# ...and we stopped trying.
|
|
120
|
+
clearInterval giscusInterval
|
|
121
|
+
, 500
|
|
122
|
+
|
|
123
|
+
# As an extra safeguard, we stop trying after a few seconds
|
|
124
|
+
# to avoid creating an infinite loop if something goes wrong.
|
|
125
|
+
setTimeout ->
|
|
126
|
+
clearInterval giscusInterval
|
|
127
|
+
, 4000 # Stop trying after 4 seconds
|
|
128
|
+
|
|
129
|
+
# giscus theme
|
|
130
|
+
#-------------------------------------------------------------------------------------------------
|
|
131
|
+
giscus = document.getElementById "giscus"
|
|
132
|
+
if giscus
|
|
133
|
+
window.giscusThemes
|
|
134
|
+
light: "{{ blog_.post.comments.giscus.theme_light | default: 'light' }}"
|
|
135
|
+
dark: "{{ blog_.post.comments.giscus.theme_dark | default: 'dark' }}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# highlight code
|
|
139
|
+
#-------------------------------------------------------------------------------------------------
|
|
140
|
+
document.querySelectorAll("div.highlight, figure.highlight").forEach (highlightBlock) ->
|
|
141
|
+
container = document.createElement "div"
|
|
142
|
+
container.className = "code-block-container"
|
|
143
|
+
header = document.createElement "div"
|
|
144
|
+
header.className = "code-block-header"
|
|
145
|
+
|
|
146
|
+
# button copy
|
|
147
|
+
button = document.createElement "button"
|
|
148
|
+
button.className = "copy-btn"
|
|
149
|
+
button.type = "button"
|
|
150
|
+
button.setAttribute "aria-label", "Copy code"
|
|
151
|
+
|
|
152
|
+
icon = document.createElement "i"
|
|
153
|
+
icon.className = "fa-solid fa-clipboard"
|
|
154
|
+
button.appendChild icon
|
|
155
|
+
|
|
156
|
+
header.appendChild button
|
|
157
|
+
|
|
158
|
+
highlightBlock.parentNode.insertBefore container, highlightBlock
|
|
159
|
+
container.appendChild header
|
|
160
|
+
container.appendChild highlightBlock
|
|
161
|
+
|
|
162
|
+
# event copy
|
|
163
|
+
button.addEventListener "click", ->
|
|
164
|
+
codeElement = highlightBlock.querySelector "td.code"
|
|
165
|
+
textToCopy = if codeElement then codeElement.innerText.trim() else highlightBlock.innerText.trim()
|
|
166
|
+
|
|
167
|
+
navigator.clipboard.writeText(textToCopy).then ->
|
|
168
|
+
icon.className = "fa-solid fa-check"
|
|
169
|
+
setTimeout (-> icon.className = "fa-solid fa-clipboard"), 2000
|
|
170
|
+
, ->
|
|
171
|
+
icon.className = "fa-solid fa-xmark"
|
|
172
|
+
setTimeout (-> icon.className = "fa-solid fa-clipboard"), 2000
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
{%- include layout/data.liquid -%}
|
|
5
|
+
|
|
6
|
+
document.addEventListener "DOMContentLoaded", ->
|
|
7
|
+
# discus
|
|
8
|
+
# ------------------------------------------------------------------------------------------------
|
|
9
|
+
discus = document.getElementById 'disqus_thread'
|
|
10
|
+
|
|
11
|
+
if discus
|
|
12
|
+
###*
|
|
13
|
+
* RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES
|
|
14
|
+
###
|
|
15
|
+
disqus_shortname = '{{ blog_.post.comments.disqus.shortname }}'
|
|
16
|
+
|
|
17
|
+
disqus_config = ->
|
|
18
|
+
@page.url = '{{ page.url | absolute_url }}' # substitua pelo seu permalink completo
|
|
19
|
+
@page.identifier = '{{ page.id }}' # ID unico para a discussão
|
|
20
|
+
@page.disable_ads = true # desativa anuncios
|
|
21
|
+
@page.recommendations = false # sesativa recomendações
|
|
22
|
+
|
|
23
|
+
do ->
|
|
24
|
+
d = document
|
|
25
|
+
s = d.createElement 'script'
|
|
26
|
+
|
|
27
|
+
s.src = '//' + disqus_shortname + '.disqus.com/embed.js'
|
|
28
|
+
s.setAttribute 'data-timestamp', +new Date()
|
|
29
|
+
|
|
30
|
+
(d.head or d.body).appendChild s
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
{%- include layout/data.liquid -%}
|
|
5
|
+
|
|
6
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
7
|
+
const btn = document.getElementById('blog-search__btn');
|
|
8
|
+
const box = document.querySelector('.blog-search');
|
|
9
|
+
const searchInput = document.getElementById('blog-search__input');
|
|
10
|
+
const blogPosts = document.getElementById('posts');
|
|
11
|
+
const searchResults = document.getElementById('blog-search__results');
|
|
12
|
+
const searchResultsWrapper = document.getElementById('blog-search__results-wrapper');
|
|
13
|
+
const btnSearchClean = document.getElementById('blog-search__btn-clean');
|
|
14
|
+
const blogSeachInput = document.getElementById('blog-search__input');
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if (!btn || !box) return;
|
|
18
|
+
|
|
19
|
+
const openSearch = () => {
|
|
20
|
+
box.classList.add('is-open');
|
|
21
|
+
box.removeAttribute('inert');
|
|
22
|
+
box.style.maxHeight = box.scrollHeight + 'px';
|
|
23
|
+
box.style.opacity = '1';
|
|
24
|
+
box.addEventListener('transitionend', function onOpened(e) {
|
|
25
|
+
if (e.propertyName === 'max-height') {
|
|
26
|
+
box.style.maxHeight = 'none';
|
|
27
|
+
box.removeEventListener('transitionend', onOpened);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
blogSeachInput.focus();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const closeSearch = () => {
|
|
34
|
+
box.style.maxHeight = box.scrollHeight + 'px';
|
|
35
|
+
void box.offsetHeight; // reflow force
|
|
36
|
+
requestAnimationFrame(() => {
|
|
37
|
+
box.style.maxHeight = '0';
|
|
38
|
+
box.style.opacity = '0';
|
|
39
|
+
});
|
|
40
|
+
box.setAttribute('inert', '');
|
|
41
|
+
box.classList.remove('is-open');
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
btn.addEventListener('click', (e) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
|
|
47
|
+
// if are already in /blog/, toggle
|
|
48
|
+
const pathname = location.pathname.replace(/\/$/, '');
|
|
49
|
+
const isBlog = pathname === '/blog' || pathname === '/blog/index.html';
|
|
50
|
+
|
|
51
|
+
if (!isBlog) {
|
|
52
|
+
// if are on another page, go to /blog/ and open it
|
|
53
|
+
window.location.href = "{{ search_url }}";
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// toggle
|
|
58
|
+
if (box.classList.contains('is-open')) {
|
|
59
|
+
closeSearch();
|
|
60
|
+
searchInput.value = '';
|
|
61
|
+
blogPosts.classList.remove('disabled');
|
|
62
|
+
searchResultsWrapper.classList.add('disabled');
|
|
63
|
+
} else {
|
|
64
|
+
openSearch();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// opens automatically if arrived from another link with ?search=open
|
|
69
|
+
const params = new URLSearchParams(location.search);
|
|
70
|
+
if (params.get('search') === 'open') {
|
|
71
|
+
setTimeout(openSearch, 30);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* clean button input blog search
|
|
75
|
+
--------------------------------------------------------------------------------------------------
|
|
76
|
+
*/
|
|
77
|
+
function clearSearch() {
|
|
78
|
+
blogSeachInput.value = '';
|
|
79
|
+
blogPosts.classList.remove('disabled');
|
|
80
|
+
searchResults.classList.add('disabled');
|
|
81
|
+
searchResultsWrapper.classList.add('disabled');
|
|
82
|
+
blogSeachInput.focus();
|
|
83
|
+
}
|
|
84
|
+
btnSearchClean.addEventListener('click', clearSearch);
|
|
85
|
+
document.addEventListener('keydown', (e) => {
|
|
86
|
+
if (e.key === 'Escape') {
|
|
87
|
+
clearSearch();
|
|
88
|
+
closeSearch();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/* open results and close posts in search (toggle)
|
|
93
|
+
--------------------------------------------------------------------------------------------------
|
|
94
|
+
*/
|
|
95
|
+
searchInput.addEventListener('input', () => {
|
|
96
|
+
if (searchInput.value.trim().length > 0) {
|
|
97
|
+
blogPosts.classList.add('disabled');
|
|
98
|
+
searchResults.classList.remove('disabled');
|
|
99
|
+
searchResultsWrapper.classList.remove('disabled');
|
|
100
|
+
} else {
|
|
101
|
+
blogPosts.classList.remove('disabled');
|
|
102
|
+
searchResults.classList.add('disabled');
|
|
103
|
+
searchResultsWrapper.classList.add('disabled');
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
var sjs = SimpleJekyllSearch({
|
|
107
|
+
searchInput: document.getElementById('blog-search__input'),
|
|
108
|
+
resultsContainer: document.getElementById('blog-search__results'),
|
|
109
|
+
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>',
|
|
110
|
+
noResultsText: '<p>{{ blog_.no_results | default: "No results found" }}</p>',
|
|
111
|
+
json: "{{ '/assets/json/blog_search.json' | relative_url }}"
|
|
112
|
+
})
|
|
113
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
{%- include layout/data.liquid -%}
|
|
5
|
+
|
|
6
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
7
|
+
const form = document.getElementById("contactForm");
|
|
8
|
+
const submitButton = document.getElementById("submitButton");
|
|
9
|
+
const endpoint = "{{ head_.google.apps_script.url }}"; // URL Google Apps Script
|
|
10
|
+
|
|
11
|
+
// get modal
|
|
12
|
+
function showModal(title, message, type = 'success') {
|
|
13
|
+
const modalEl = document.getElementById('contactMessageModal');
|
|
14
|
+
const modalTitle = modalEl.querySelector('.modal-title');
|
|
15
|
+
const modalBody = modalEl.querySelector('.modal-body');
|
|
16
|
+
const modalContent = modalEl.querySelector('.modal-content');
|
|
17
|
+
|
|
18
|
+
modalContent.classList.remove('contact-message-success', 'contact-message-error', 'contact-message-warning');
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
// Apply the color according to the type
|
|
22
|
+
if (type === 'success') {
|
|
23
|
+
modalContent.classList.add('contact-message-success');
|
|
24
|
+
} else if (type === 'error') {
|
|
25
|
+
modalContent.classList.add('contact-message-error');
|
|
26
|
+
} else if (type === 'warning') {
|
|
27
|
+
modalContent.classList.add('contact-message-warning');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
modalTitle.innerHTML = title;
|
|
31
|
+
modalBody.innerHTML = message;
|
|
32
|
+
|
|
33
|
+
const bsModal = new bootstrap.Modal(modalEl);
|
|
34
|
+
bsModal.show();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
form.addEventListener("submit", async (e) => {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
|
|
40
|
+
const recaptchaResponse = grecaptcha.getResponse();
|
|
41
|
+
if (!recaptchaResponse) {
|
|
42
|
+
showModal(
|
|
43
|
+
"{{ contact_.recaptcha.warning.title | default: 'Warning' }}",
|
|
44
|
+
`{{ contact_.recaptcha.warning.content | default: "Please tick the 'I'm not a robot' box." }}`,
|
|
45
|
+
"warning"
|
|
46
|
+
);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const textarea = document.getElementById('textMessage');
|
|
51
|
+
const text = textarea.value.trim();
|
|
52
|
+
if (text.length < "{{ contact_.message.caracters.min }}" ) {
|
|
53
|
+
showModal(
|
|
54
|
+
"{{ contact_.message.caracters.warning.title | default: 'Warning' }}",
|
|
55
|
+
"{{ contact_.message.caracters.warning.content | default: 'The message must have at least 50 characters.' }}",
|
|
56
|
+
"warning"
|
|
57
|
+
);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
submitButton.disabled = true;
|
|
62
|
+
submitButton.textContent = "{{ contact_.message.status | default: 'Sending...Wait' }}";
|
|
63
|
+
|
|
64
|
+
const formData = new FormData(form);
|
|
65
|
+
const data = Object.fromEntries(formData.entries());
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(endpoint, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
redirect: "follow",
|
|
71
|
+
body: JSON.stringify(data)
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const result = await response.json();
|
|
75
|
+
|
|
76
|
+
if (result.result === 'success') {
|
|
77
|
+
form.reset();
|
|
78
|
+
grecaptcha.reset();
|
|
79
|
+
showModal(
|
|
80
|
+
"{{ contact_.message.success.title | default: 'Message Sent' }}",
|
|
81
|
+
"{{ contact_.message.success.content | default: 'Your message has been sent successfully!' }}",
|
|
82
|
+
"success"
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
showModal(
|
|
86
|
+
"{{ contact_.message.error.title | default: 'Error' }}",
|
|
87
|
+
"{{ contact_.message.error.content | default: 'Something went wrong while sending your message.' }}",
|
|
88
|
+
"error"
|
|
89
|
+
);
|
|
90
|
+
throw new Error(result.message || "{{ contact_.message.error.content | default: 'An unknown error has occurred.' }}");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error("Error sending:", error);
|
|
95
|
+
if (error.message.includes("reCAPTCHA")) {
|
|
96
|
+
showModal(
|
|
97
|
+
"{{ contact_.message.error.title | default: 'Error' }}",
|
|
98
|
+
"{{ contact_.recaptcha.fail | default: 'Verification failed. Please reload the page and try again.' }}",
|
|
99
|
+
"error"
|
|
100
|
+
);
|
|
101
|
+
} else {
|
|
102
|
+
showModal(
|
|
103
|
+
"{{ contact_.message.error.title | default: 'Error' }}",
|
|
104
|
+
"{{ contact_.recaptcha.error | default: 'An error occurred while sending the message. Please try again.' }}",
|
|
105
|
+
"error"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
grecaptcha.reset();
|
|
109
|
+
|
|
110
|
+
} finally {
|
|
111
|
+
submitButton.disabled = false;
|
|
112
|
+
submitButton.textContent = "{{ contact_.button.text | default: 'Send!' }}";
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
});
|
|
@@ -1,9 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
|
|
1
4
|
document.addEventListener("DOMContentLoaded", () => {
|
|
2
5
|
/* lock menu context (click right mouse)
|
|
3
6
|
--------------------------------------------------------------------------------------------------
|
|
4
7
|
*/
|
|
5
8
|
document.addEventListener('contextmenu', e => e.preventDefault());
|
|
6
9
|
|
|
10
|
+
/* avatar
|
|
11
|
+
-------------------------------------------------------------------------------------------------
|
|
12
|
+
*/
|
|
13
|
+
const modalEl = document.getElementById('avatarModal');
|
|
14
|
+
if (modalEl) {
|
|
15
|
+
const flipperAvatars = document.querySelectorAll('.avatar-flipper__open-true');
|
|
16
|
+
const modalAvatar = document.getElementById('modalAvatar');
|
|
17
|
+
const header = document.querySelector('.header');
|
|
18
|
+
const bsModal = new bootstrap.Modal(modalEl);
|
|
19
|
+
|
|
20
|
+
flipperAvatars.forEach((flipper) => {
|
|
21
|
+
flipper.addEventListener("click", () => {
|
|
22
|
+
const card = flipper.querySelector('.avatar-card');
|
|
23
|
+
const backImage = flipper.querySelector('.avatar-back img');
|
|
24
|
+
const backImageSrc = backImage.src;
|
|
25
|
+
|
|
26
|
+
card.classList.add("flip-avatar");
|
|
27
|
+
|
|
28
|
+
card.addEventListener(
|
|
29
|
+
"animationend",
|
|
30
|
+
() => {
|
|
31
|
+
card.classList.remove("flip-avatar");
|
|
32
|
+
|
|
33
|
+
modalAvatar.src = backImageSrc;
|
|
34
|
+
|
|
35
|
+
bsModal.show();
|
|
36
|
+
},
|
|
37
|
+
{ once: true }
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
modalEl.addEventListener("shown.bs.modal", () => {
|
|
43
|
+
modalAvatar.classList.remove("modal-avatar");
|
|
44
|
+
void modalAvatar.offsetWidth;
|
|
45
|
+
modalAvatar.classList.add("modal-avatar");
|
|
46
|
+
header.classList.remove("modal-active");
|
|
47
|
+
|
|
48
|
+
flipperAvatars.forEach((flipper) => flipper.classList.add("hidden"));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
modalEl.addEventListener("hidden.bs.modal", () => {
|
|
52
|
+
flipperAvatars.forEach((flipper) => flipper.classList.remove("hidden"));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
7
57
|
/* Show/disappear top button
|
|
8
58
|
--------------------------------------------------------------------------------------------------
|
|
9
59
|
*/
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
{%- include layout/data.liquid -%}
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
8
|
+
const discus = document.getElementById('disqus_thread');
|
|
9
|
+
|
|
10
|
+
if (discus) {
|
|
11
|
+
/**
|
|
12
|
+
* RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES
|
|
13
|
+
*/
|
|
14
|
+
var disqus_shortname = '{{ blog_.post.comments.disqus.shortname }}';
|
|
15
|
+
|
|
16
|
+
// The unique URL for the discussion, usually the post's permalink.
|
|
17
|
+
var disqus_config = function () {
|
|
18
|
+
this.page.url = '{{ page.url | absolute_url }}'; // Replace with your full permalink
|
|
19
|
+
this.page.identifier = '{{ page.id }}'; // Unique ID for the discussion, use page.id or page.url
|
|
20
|
+
this.page.disable_ads = true; // disabled ads
|
|
21
|
+
this.page.recommendations = false; // disabled recommendations
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
(function() {
|
|
25
|
+
var d = document, s = d.createElement('script');
|
|
26
|
+
s.src = '//' + disqus_shortname + '.disqus.com/embed.js';
|
|
27
|
+
s.setAttribute('data-timestamp', +new Date());
|
|
28
|
+
(d.head || d.body).appendChild(s);
|
|
29
|
+
})();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
{%- include layout/data.liquid -%}
|
|
5
|
+
|
|
6
|
+
|
|
2
7
|
if(!(window.doNotTrack === "1" || navigator.doNotTrack === "1" || navigator.doNotTrack === "yes" || navigator.msDoNotTrack === "1")) {
|
|
3
8
|
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
|
4
9
|
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
|
5
10
|
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
|
6
11
|
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
|
7
12
|
|
|
8
|
-
ga('create', '{{
|
|
13
|
+
ga('create', '{{ head_.google.analytics.id }}', 'auto');
|
|
9
14
|
ga('send', 'pageview');
|
|
10
15
|
}
|
|
11
|
-
</script>
|