rawfeed 0.1.4 → 0.2.1
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>
         |