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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +20 -21
  3. data/README.md +12 -130
  4. data/_data/options.yml +270 -0
  5. data/_data/resume.yml +8 -8
  6. data/_includes/alert +3 -1
  7. data/_includes/chart +13 -32
  8. data/_includes/details +1 -57
  9. data/_includes/image +12 -4
  10. data/_includes/layout/blog_search.html +6 -4
  11. data/_includes/layout/data.liquid +21 -3
  12. data/_includes/layout/disqus.html +12 -26
  13. data/_includes/layout/footer.html +33 -17
  14. data/_includes/layout/giscus.html +27 -19
  15. data/_includes/layout/head.html +41 -41
  16. data/_includes/layout/header.html +127 -101
  17. data/_includes/layout/maintenance.html +6 -10
  18. data/_includes/layout/paginator.html +6 -4
  19. data/_includes/socials +7 -5
  20. data/_includes/tabs +1 -94
  21. data/_includes/toc +11 -194
  22. data/_includes/video +4 -1
  23. data/_layouts/blog.html +8 -7
  24. data/_layouts/contact.html +90 -196
  25. data/_layouts/default.html +42 -341
  26. data/_layouts/error.html +6 -4
  27. data/_layouts/home.html +45 -36
  28. data/_layouts/licenses.html +10 -0
  29. data/_layouts/page.html +4 -4
  30. data/_layouts/pixel.html +48 -0
  31. data/_layouts/pixels.html +71 -1
  32. data/_layouts/post.html +28 -29
  33. data/_layouts/resume.html +41 -34
  34. data/_layouts/tag.html +14 -3
  35. data/_layouts/tag_posts.html +3 -3
  36. data/_sass/base/_index.scss +39 -3
  37. data/_sass/components/_badges.scss +10 -0
  38. data/_sass/components/_markdown.scss +8 -5
  39. data/_sass/includes/_footer.scss +5 -2
  40. data/_sass/includes/_header.scss +23 -19
  41. data/_sass/includes/_highlight.scss +20 -7
  42. data/_sass/includes/_maintenance.scss +2 -3
  43. data/_sass/includes/_terminal.scss +35 -12
  44. data/_sass/layouts/_blog.scss +13 -9
  45. data/_sass/layouts/_contact.scss +6 -5
  46. data/_sass/layouts/_default.scss +5 -5
  47. data/_sass/layouts/_index.scss +3 -0
  48. data/_sass/layouts/_licenses.scss +7 -0
  49. data/_sass/layouts/_page.scss +1 -0
  50. data/_sass/layouts/_pixel.scss +61 -0
  51. data/_sass/layouts/_pixels.scss +86 -0
  52. data/_sass/layouts/_post.scss +4 -11
  53. data/_sass/layouts/_resume.scss +16 -3
  54. data/_sass/layouts/_tag-posts.scss +1 -2
  55. data/_sass/layouts/_tag.scss +12 -1
  56. data/_sass/main.scss +16 -1
  57. data/_sass/theme/_dark.scss +8 -1
  58. data/_sass/theme/_light.scss +8 -1
  59. data/assets/images/blog/.keep +0 -0
  60. data/assets/images/pixels/luffy.jpg +0 -0
  61. data/assets/js/blog.coffee +102 -0
  62. data/assets/js/contact.coffee +105 -0
  63. data/assets/js/default.coffee +172 -0
  64. data/assets/js/discus.coffee +30 -0
  65. data/assets/js/fallback/README.md +3 -0
  66. data/assets/js/fallback/blog.js +113 -0
  67. data/assets/js/fallback/contact.js +116 -0
  68. data/assets/js/{default.js → fallback/default.js} +50 -0
  69. data/assets/js/fallback/discus.js +32 -0
  70. data/{_includes/layout/google_analytics.html → assets/js/fallback/google_analytics.js} +7 -3
  71. data/assets/js/fallback/home.js +275 -0
  72. data/assets/js/fallback/no_inframe.js +4 -0
  73. data/assets/js/fallback/page.js +423 -0
  74. data/assets/js/fallback/pixels.js +1 -0
  75. data/assets/js/fallback/resume.js +13 -0
  76. data/assets/js/fallback/tags.js +1 -0
  77. data/{_includes/layout/capture_scripts.liquid → assets/js/fallback/theme_load.js} +0 -2
  78. data/assets/js/google_analytics.coffee +24 -0
  79. data/assets/js/home.coffee +250 -0
  80. data/assets/js/no_inframe.coffee +9 -0
  81. data/assets/js/page.coffee +379 -0
  82. data/assets/js/pixels.coffee +2 -0
  83. data/assets/js/resume.coffee +9 -0
  84. data/assets/js/tags.coffee +2 -0
  85. data/assets/js/theme_load.coffee +6 -0
  86. data/assets/json/blog_search.json +2 -2
  87. data/lib/rawfeed/author.rb +59 -0
  88. data/lib/rawfeed/csp_filters.rb +3 -0
  89. data/lib/rawfeed/draft.rb +1 -1
  90. data/lib/rawfeed/layout.rb +7 -0
  91. data/lib/rawfeed/page.rb +2 -2
  92. data/lib/rawfeed/pixel.rb +32 -0
  93. data/lib/rawfeed/post.rb +2 -2
  94. data/lib/rawfeed/resume.rb +1 -0
  95. data/lib/rawfeed/typescript_liquid.rb +172 -0
  96. data/lib/rawfeed/utils.rb +1 -0
  97. data/lib/rawfeed/version.rb +1 -1
  98. data/lib/rawfeed/with_class.rb +20 -0
  99. data/lib/rawfeed.rb +5 -1
  100. metadata +44 -12
  101. data/assets/js/avatar.js +0 -59
  102. 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,3 @@
1
+ TODO: **If you experience issues with CoffeeScript, revert to standard Javascript.**
2
+
3
+ > Note: Maintain the fallback in parallel development with CoffeeScript.
@@ -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>&nbsp;»&nbsp; <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
- <script>
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', '{{ site.google.analytics.id }}', 'auto');
13
+ ga('create', '{{ head_.google.analytics.id }}', 'auto');
9
14
  ga('send', 'pageview');
10
15
  }
11
- </script>