diff --git a/.gitignore b/.gitignore index 35e86ad3..8f8504b1 100644 --- a/.gitignore +++ b/.gitignore @@ -85,4 +85,10 @@ _site .sass-cache .jekyll-cache .jekyll-metadata -vendor \ No newline at end of file +vendor + +# mypy files +.mypy_cache/ + +# Ruby files +.ruby-lsp/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 72e2b18a..ae9a2060 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,17 +47,25 @@ diciotech ├── _config.yml (arquivo de configuração do Jekyll) ├── _data (aqui devem ser adicionados os termos e definições) │   ├── en-us -│   │   ├── a.yml +│   │   ├── a.yml (termos e definições que começam com a letra A) │   │   ├── ... -│   │   └── z.yml +│   │   ├── numbers.yml (termos e definições que começam com números) +│   │   ├── ... +│   │   ├── strings.yml (onde ficam as traduções dos termos na página) +│   │   ├── symbols.yml (termos e definições que começam com símbolos) +│   │   ├── ... +│   │   └── z.yml (termos e definições que começam com a letra Z) │   └── pt-br -│   ├── a.yml -│      ├── ... -│   └── z.yml +│   │   ├── a.yml (termos e definições que começam com a letra A) +│   │   ├── ... +│   │   ├── numbers.yml (termos e definições que começam com números) +│   │   ├── ... +│   │   ├── strings.yml (onde ficam as traduções dos termos na página) +│   │   ├── symbols.yml (termos e definições que começam com símbolos) +│   │   ├── ... +│   │   └── z.yml (termos e definições que começam com a letra Z) ├── Gemfile (arquivo de dependências do Ruby) ├── Gemfile.lock (arquivo de dependências do Ruby com as versões específicas) -├── _includes -│   └── script.liquid.js (aqui devem ficar códigos js que dependem de valores do Jekyll) ├── _json (cria os cards.json final a partir dos yml) │   ├── en-us │   │   └── cards.json @@ -65,28 +73,30 @@ diciotech │   └── cards.json ├── _layouts │   └── base.liquid (layout base do site) -├── _pages (onde ficam as traduções dos termos na página) +├── _pages │   ├── en-us -│   │   └── search.md +│   │   └── search.md (define que vai ter uma página search em inglês) │   └── pt-br -│   └── search.md +│   └── search.md (define que vai ter uma página search em português) ├── _sass (onde devem ser feitas as mudanças no estilo do site) │   ├── base.scss │   ├── cookies.scss │   ├── dark_theme.scss │   ├── light_theme.scss │   └── variables.scss +├── _scripts (aqui devem ficar códigos js que dependem de valores do Jekyll) +│   └── scripts.js.liquid └── _site (onde o Jekyll gera o site final, não deve ser versionado) └── ... ``` Dentre os arquivos e pastas, os mais importantes são: -- `_data/`: onde ficam os arquivos YAML com os termos e definições, separados por idioma e letra. Aqui é onde você deve adicionar novos termos; -- `_includes/`: onde ficam códigos js que dependem de valores do Jekyll. Você pode adicionar códigos novos no arquivo `script.liquid.js`, ou criar novos arquivos. Lembre-se de incluir os arquivos novos no layout base; -- `_layouts/base.liquid`: layout base da página, basicamente um html com variáveis em liquid definidas em `_pages/`; -- `_pages/`: onde ficam as traduções de textos na página que não são termos e suas definições; +- `_data/`: onde ficam os arquivos YAML com os termos e definições do dicionário, separados por idioma e letra (com exceção do arquivo `strings.yml` explicado abaixo). Aqui é onde você deve adicionar novos termos; +- `_data/LANG/strings.yml`: onde ficam as traduções dos termos na página que não são termos do dicionário; +- `_layouts/base.liquid`: layout base da página, basicamente um html com variáveis em liquid definidas em `_data/LANG/strings.yml`; - `_sass/`: onde ficam os arquivos de estilo do site. Aqui é onde você deve fazer mudanças de css; +- `_scripts/`: onde ficam códigos js que dependem de valores do Jekyll. Você pode adicionar códigos novos no arquivo `scripts.js.liquid`, ou criar novos arquivos. Lembre-se de incluir os arquivos novos no layout base; - `assets/`: onde ficam os arquivos de css, js e imagens que são copiados tal qual para o site final. ## Como funciona o build do site @@ -128,12 +138,15 @@ _site/ │   ├── cookies.js │   ├── levenshtein.js │   ├── pwa.js +│   ├── scripts.js │   └── theme.js ├── diciotech.webmanifest ├── en-us │   ├── assets -│   │   └── data -│   │   └── cards.json +│   │   ├── data +│   │   | └── cards.json +| | └── js +| | └── scripts.js │   ├── diciotech.webmanifest │   └── index.html └── index.html diff --git a/_config.yml b/_config.yml index e6bdcebd..5a5bfc7d 100644 --- a/_config.yml +++ b/_config.yml @@ -6,7 +6,7 @@ url: "https://diciotech.netlify.app/" # the base hostname & protocol for your si # ----------------------------------------------------------------------------- # Jekyll Polyglot -languages: ["en-us", "pt-br"] +languages: ["pt-br", "en-us"] default_lang: "pt-br" exclude_from_localization: ["assets"] lang_from_path: true @@ -36,7 +36,7 @@ exclude: - run_on_data_changed.sh - todo.md -include: ["_json", "_pages"] +include: ["_json", "_pages", "_scripts"] plugins: - jekyll-minifier diff --git a/_data/en-us/strings.yml b/_data/en-us/strings.yml new file mode 100644 index 00000000..2e05c65e --- /dev/null +++ b/_data/en-us/strings.yml @@ -0,0 +1,18 @@ +accept_cookies: Accept and close +all: All +change_theme: Change theme +cookies_message: Diciotech uses cookies to ensure you get a better experience. Disabling site cookies may impair the functionality of some features and by continuing to use, you agree to our Cookie Policy. +currently_with: Currently with +favorites: Favorites +filter_by_category: "Filter by category:" +no_results: + alt: Woman looking at site with no data + text: | +

The searched term was not found! Check if it was typed correctly.

+

If it is correct, please create an issue in the repository so that this term can be added to Diciotech.

+

We appreciate your collaboration! 😄

+read_more: Read more +search_placeholder: Search... +site_description: A tech dictionary for people who want to learn more about technical terms within technology! +terms: terms! +view_on_github: View project on GitHub diff --git a/_data/pt-br/strings.yml b/_data/pt-br/strings.yml new file mode 100644 index 00000000..1d10be5f --- /dev/null +++ b/_data/pt-br/strings.yml @@ -0,0 +1,18 @@ +accept_cookies: Concordar e fechar +all: Todos +change_theme: Mudar tema +cookies_message: O Diciotech usa cookies para garantir que você obtenha uma melhor experiência. Desativar os cookies do site pode prejudicar a funcionalidade de alguns recursos e ao continuar usando, você concorda com a nossa Política de Cookies. +currently_with: Atualmente com +favorites: Favoritos +filter_by_category: "Filtrar por categoria:" +no_results: + alt: Mulher olhando para site sem dados + text: | +

O termo pesquisado não foi encontrado! Verifique se foi digitado corretamente.

+

Caso esteja correto, por favor crie uma issue no repositório para que esse termo possa ser adicionado ao Diciotech.

+

Agradecemos sua colaboração! 😄

+read_more: Ler mais +search_placeholder: Pesquisar... +site_description: Um dicionário tech para pessoas que querem aprender mais sobre termos técnicos dentro da tecnologia! +terms: termos! +view_on_github: Visualizar projeto no GitHub diff --git a/_includes/script.js.liquid b/_includes/script.js.liquid deleted file mode 100644 index 383ca760..00000000 --- a/_includes/script.js.liquid +++ /dev/null @@ -1,341 +0,0 @@ - diff --git a/_json/en-us/cards.json.liquid b/_json/en-us/cards.json.liquid index 061aa17a..32d7efbe 100644 --- a/_json/en-us/cards.json.liquid +++ b/_json/en-us/cards.json.liquid @@ -2,21 +2,23 @@ id: cards permalink: /assets/data/cards.json --- -{ "cards": [ +{"cards":[ {%- for letter in site.data[site.active_lang] -%} - {%- for card in letter[1] -%} - { "title": "{{ card.title | escape }}", "tags": [ - {%- for tag in card.tags -%} - "{{ tag }}" + {%- if letter[0] != "strings" -%} + {%- for card in letter[1] -%} + { "title": "{{ card.title | escape }}", "tags": [ + {%- for tag in card.tags -%} + "{{ tag }}" + {%- unless forloop.last -%},{%- endunless -%} + {%- endfor -%} + ], "description": "{{ card.description | escape }}" + {%- if card.content -%} + ,"content": { "code": "{{ card.content.code | escape }}" } + {%- endif -%} + } {%- unless forloop.last -%},{%- endunless -%} {%- endfor -%} - ], "description": "{{ card.description | escape }}" - {%- if card.content -%} - ,"content": { "code": "{{ card.content.code | escape }}" } - {%- endif -%} - } {%- unless forloop.last -%},{%- endunless -%} - {%- endfor -%} - {%- unless forloop.last -%},{%- endunless -%} + {%- endif -%} {%- endfor -%} -] } +]} diff --git a/_json/pt-br/cards.json.liquid b/_json/pt-br/cards.json.liquid index 061aa17a..32d7efbe 100644 --- a/_json/pt-br/cards.json.liquid +++ b/_json/pt-br/cards.json.liquid @@ -2,21 +2,23 @@ id: cards permalink: /assets/data/cards.json --- -{ "cards": [ +{"cards":[ {%- for letter in site.data[site.active_lang] -%} - {%- for card in letter[1] -%} - { "title": "{{ card.title | escape }}", "tags": [ - {%- for tag in card.tags -%} - "{{ tag }}" + {%- if letter[0] != "strings" -%} + {%- for card in letter[1] -%} + { "title": "{{ card.title | escape }}", "tags": [ + {%- for tag in card.tags -%} + "{{ tag }}" + {%- unless forloop.last -%},{%- endunless -%} + {%- endfor -%} + ], "description": "{{ card.description | escape }}" + {%- if card.content -%} + ,"content": { "code": "{{ card.content.code | escape }}" } + {%- endif -%} + } {%- unless forloop.last -%},{%- endunless -%} {%- endfor -%} - ], "description": "{{ card.description | escape }}" - {%- if card.content -%} - ,"content": { "code": "{{ card.content.code | escape }}" } - {%- endif -%} - } {%- unless forloop.last -%},{%- endunless -%} - {%- endfor -%} - {%- unless forloop.last -%},{%- endunless -%} + {%- endif -%} {%- endfor -%} -] } +]} diff --git a/_layouts/base.liquid b/_layouts/base.liquid index 57678825..a6115513 100644 --- a/_layouts/base.liquid +++ b/_layouts/base.liquid @@ -7,7 +7,7 @@ @@ -33,7 +33,7 @@ > - + @@ -46,7 +46,7 @@ href="https://github.com/levxyca/diciotech" target="_blank" class="github-corner" - aria-label="{{ page.view_on_github }}" + aria-label="{{ site.data[site.active_lang].strings.view_on_github }}" >

- {{ page.site_description }} 📖
- {{ page.currently_with }} - 0 {{ page.terms }} 🚀 + {{ site.data[site.active_lang].strings.site_description }} 📖
+ {{ site.data[site.active_lang].strings.currently_with }} + 0 {{ site.data[site.active_lang].strings.terms }} 🚀

@@ -156,7 +156,7 @@ + - {% include script.js.liquid %} - - + + + diff --git a/_pages/en-us/search.md b/_pages/en-us/search.md index 71fd8002..1858b160 100644 --- a/_pages/en-us/search.md +++ b/_pages/en-us/search.md @@ -2,23 +2,4 @@ page_id: search layout: base permalink: / - -accept_cookies: Accept and close -all: All -change_theme: Change theme -cookies_message: Diciotech uses cookies to ensure you get a better experience. Disabling site cookies may impair the functionality of some features and by continuing to use, you agree to our Cookie Policy. -currently_with: Currently with -favorites: Favorites -filter_by_category: "Filter by category:" -no_results: - alt: Woman looking at site with no data - text: | -

The searched term was not found! Check if it was typed correctly.

-

If it is correct, please create an issue in the repository so that this term can be added to Diciotech.

-

We appreciate your collaboration! 😄

-read_more: Read more -search_placeholder: Search... -site_description: A tech dictionary for people who want to learn more about technical terms within technology! -terms: terms! -view_on_github: View project on GitHub --- diff --git a/_pages/pt-br/search.md b/_pages/pt-br/search.md index 4d4ff509..1858b160 100644 --- a/_pages/pt-br/search.md +++ b/_pages/pt-br/search.md @@ -2,23 +2,4 @@ page_id: search layout: base permalink: / - -accept_cookies: Concordar e fechar -all: Todos -change_theme: Mudar tema -cookies_message: O Diciotech usa cookies para garantir que você obtenha uma melhor experiência. Desativar os cookies do site pode prejudicar a funcionalidade de alguns recursos e ao continuar usando, você concorda com a nossa Política de Cookies. -currently_with: Atualmente com -favorites: Favoritos -filter_by_category: "Filtrar por categoria:" -no_results: - alt: Mulher olhando para site sem dados - text: | -

O termo pesquisado não foi encontrado! Verifique se foi digitado corretamente.

-

Caso esteja correto, por favor crie uma issue no repositório para que esse termo possa ser adicionado ao Diciotech.

-

Agradecemos sua colaboração! 😄

-read_more: Ler mais -search_placeholder: Pesquisar... -site_description: Um dicionário tech para pessoas que querem aprender mais sobre termos técnicos dentro da tecnologia! -terms: termos! -view_on_github: Visualizar projeto no GitHub --- diff --git a/_scripts/scripts.js.liquid b/_scripts/scripts.js.liquid new file mode 100644 index 00000000..496247fa --- /dev/null +++ b/_scripts/scripts.js.liquid @@ -0,0 +1,355 @@ +--- +permalink: /assets/js/scripts.js +--- +import { levenshtein } from '/assets/js/levenshtein.js'; + +// variables that need to be set by jekyll +const allTag = '{{ site.data[site.active_lang].strings.all }}'; +const favoriteTag = '{{ site.data[site.active_lang].strings.favorites }}'; +const noResultsAlt = '{{ site.data[site.active_lang].strings.no_results.alt }}'; +const noResultsText = `{{ site.data[site.active_lang].strings.no_results.text }}`; + +const exactWordScore = 12; +const partialWordScore = 10; +const levenshteinScore = 10; +const levenshteinThreshold = 3; + +const searchInput = document.querySelector('#search-input'); +const cardsSection = document.querySelector('#cards'); +const filterSelect = document.querySelector('#tags-filter'); +let listOfCardsFiltered = []; +let favoriteCards = []; + +function insertTagsIntoSelect(tags) { + tags.sort(); + for (const tag of tags) { + const newOption = document.createElement('option'); + newOption.value = tag; + newOption.text = tag; + filterSelect.appendChild(newOption); + } +} + +function getTagsFromCards(data) { + const tags = [favoriteTag]; + data.map((objeto) => { + if (objeto.tags) { + objeto.tags.map((tag) => { + if (!tags.includes(tag)) { + tags.push(tag); + } + }); + } else { + objeto.tags = []; + } + }); + insertTagsIntoSelect(tags); +} + +function filterCards() { + listOfCardsFiltered = []; + const listOfCards = document.querySelectorAll('.card'); + listOfCards.forEach((element) => { + if (element.getAttribute('tags').includes(filterSelect.value) || filterSelect.value == allTag) { + element.style.display = ''; + listOfCardsFiltered.push(element); + } else { + element.style.display = 'none'; + } + }); + searchCards(); +} + +function sortCards(sortingArray) { + if (listOfCardsFiltered.length > 0) { + if (!Array.isArray(sortingArray) || !sortingArray.length) { + const cards = document.querySelector('#cards'); + // selects all cards that are not hidden and sorts them by title + // every child is re-appended to cards in the order of the now sorted array. When an element is re-appended it is actually moved from its previous location + [...cards.querySelectorAll(".card:not([style*='display: none;'])")] + .sort((a, b) => + a + .querySelector('.card__title') + .textContent.toLowerCase() + .localeCompare(b.querySelector('.card__title').textContent.toLowerCase()), + ) + .forEach((node) => cards.appendChild(node)); + } else { + const cards = document.querySelector('#cards'); + // selects all cards that are not hidden and sorts them by the order of the sortingArray + // every child is re-appended to cards in the order of the now sorted array. When an element is re-appended it is actually moved from its previous location + [...cards.querySelectorAll(".card:not([style*='display: none;'])")] + .sort((a, b) => sortingArray.indexOf(a) - sortingArray.indexOf(b)) + .forEach((node) => cards.appendChild(node)); + } + } +} + +function searchCards() { + const inputValue = searchInput.value.toLowerCase().trim(); + let cardsScores = []; + + if (inputValue.length > 0) { + const searchWords = inputValue.split(/\s+/); + + for (const card of listOfCardsFiltered) { + let cardScore = 0; + + // search for words inside the title that either contains the search words or have a low levenshtein distance + // only consider the best case for each search word + const cardTitle = card.querySelector('.card__title').textContent.toLowerCase(); + const titleWords = cardTitle.split(/\s+/); + let titleScore = 0; + + searchWords.forEach((searchWord) => { + let wordScore = 0; + + titleWords.some((word) => { + if (word == searchWord) { + // breaks the loop if the word is an exact match, since no other word can have a higher score + wordScore = exactWordScore; + return true; + } else if (wordScore < partialWordScore) { + if (word.includes(searchWord)) { + wordScore = partialWordScore; + } else if (word.length > 3) { + const levenshteinDistance = levenshtein(searchWord, word); + + // only the word with the lowest levenshtein distance will be considered + if (levenshteinDistance <= levenshteinThreshold && levenshteinScore - levenshteinDistance > wordScore) { + wordScore = levenshteinScore - levenshteinDistance; + } + } + } + }); + + titleScore += wordScore; + }); + + // give extra points for words in title + cardScore += titleScore * 10; + + // search for words inside the description that either contains the search words or have a low levenshtein distance + // only consider the best case for each search word + const cardDescription = card.querySelector('.card__description').textContent.toLowerCase(); + const descriptionWords = cardDescription.split(/\s+/); + let descriptionScore = 0; + + searchWords.forEach((searchWord) => { + let wordScore = 0; + + descriptionWords.some((word) => { + if (word == searchWord) { + // breaks the loop if the word is an exact match, since no other word can have a higher score + wordScore = exactWordScore; + return true; + } else if (wordScore < partialWordScore) { + if (word.includes(searchWord)) { + wordScore = partialWordScore; + } else if (word.length > 3) { + const levenshteinDistance = levenshtein(searchWord, word); + + // only the word with the lowest levenshtein distance will be considered + if (levenshteinDistance <= levenshteinThreshold && levenshteinScore - levenshteinDistance > wordScore) { + wordScore = levenshteinScore - levenshteinDistance; + } + } + } + }); + + descriptionScore += wordScore; + }); + + cardScore += descriptionScore; + + if (cardScore > 0) { + card.style.display = ''; + cardsScores.push([card, cardScore]); + } else { + card.style.display = 'none'; + } + } + + const msgNotFound = document.querySelector('div.msg'); + + if (cardsScores.length > 0) { + msgNotFound.style.display = 'none'; + // sort the array of cards by score + cardsScores.sort((a, b) => b[1] - a[1]); + // remove the scores from the array + cardsScores = cardsScores.map((card) => card[0]); + sortCards(cardsScores); + } else { + msgNotFound.style.display = ''; + } + } else { + // display all cards if search input is empty + for (const card of listOfCardsFiltered) { + card.style.display = ''; + cardsScores.push(card); + } + + const msgNotFound = document.querySelector('div.msg'); + msgNotFound.style.display = 'none'; + sortCards(); + } +} + +function insertCardsIntoHtml(data) { + console.log(data[1]); + let cards = `
+
+ ${noResultsAlt} + Data illustrations by Storyset +
+
+ ${noResultsText} +
+
`; + data.forEach((card) => { + const cardId = generateCardId(card.id, card.title, card.description); + cards += ` +
+
+

${card.title}

+ + +
+

${card.description}

+ `; + if (card.content && card.content.code) { + cards += ` +
+ ${card.content.code} +
+ `; + } + cards += '
'; + }); + cardsSection.innerHTML = cards; + + const favButtons = document.querySelectorAll('.fav__button'); + favButtons.forEach((button) => { + button.addEventListener('click', () => { + setCardAsFavorite(button.getAttribute('unique-title')); + }); + }); + + filterCards(); +} + +function addFavoriteTagToCard(cardId) { + const card = document.getElementById(cardId); + const tags = card.getAttribute('tags').split(','); + + if (tags.includes(favoriteTag)) { + tags.splice(tags.indexOf(favoriteTag), 1); + } else { + tags.push(favoriteTag); + } + + card.setAttribute('tags', tags); +} + +function setCardAsFavorite(cardId) { + const favIcon = document.querySelector(`#fav_${cardId}`); + + if (favoriteCards.includes(cardId)) { + favIcon.className = 'ph ph-star fav__button'; + favoriteCards.splice(favoriteCards.indexOf(cardId), 1); + } else { + favIcon.className = 'ph-fill ph-star fav__button'; + favoriteCards.push(cardId); + } + + addFavoriteTagToCard(cardId); + + localStorage.setItem('favoriteCards', favoriteCards); +} + +async function loadFavoriteCardsId() { + const cardsId = localStorage.getItem('favoriteCards'); + if (cardsId) { + favoriteCards = cardsId.split(','); + } +} + +async function addFavoriteTag(cards) { + cards.map((card) => { + const cardId = generateCardId(card.id, card.title, card.description); + if (favoriteCards.includes(cardId)) { + if (!card.tags) { + card.tags = []; + } + card.tags.push(favoriteTag); + } + }); + return cards; +} + +async function sortCardsByTitle(data) { + return data.cards.sort((a, b) => a.title.localeCompare(b.title)); +} + +async function getCardsFromJson() { + try { + {% if site.active_lang == site.default_lang -%} + const res = await fetch('{{ "/assets/data/cards.json" | relative_url }}'); + {%- else -%} + const res = await fetch('{{ "/assets/data/cards.json" | prepend: site.active_lang | prepend: "/" | relative_url }}'); + {%- endif %} + console.log("{{ site.active_lang }}"); + const data = await res.json(); + console.log(data); + const sortedCards = await sortCardsByTitle(data); + document.getElementById('total-terms').textContent = sortedCards.length; + await loadFavoriteCardsId(); + await addFavoriteTag(sortedCards); + getTagsFromCards(sortedCards); + insertCardsIntoHtml(sortedCards); + } catch (error) { + console.error('An error occurred while fetching card data.', error); + } +} + +searchInput.addEventListener('input', searchCards); +filterSelect.addEventListener('change', filterCards); +getCardsFromJson(); + +/** + * Generates a card ID using a default UUID or a hash of the card description. + * + * @param {string} defaultCardId - A default UUID generated by the CLI. + * @param {string} title - The title of the card. + * @param {string} description - The description of the card. + * @returns {string} - A generated ID + */ +function generateCardId(defaultCardId, title, description) { + if (defaultCardId) return defaultCardId; + return generateContentId(title, description); +} + +/** + * Calculates a simple hash of the given content. + * + * @param {string} content - The content to be hashed. + * @param {string} title - An additional title to be added to the content. + * @param {number} hash - The initial hash value. + * @returns {string} The hashed representation of the content. + */ +function generateContentId(title = '', description = '', hash = 5381) { + const data = (title + description).slice(0, 32).split(' ').join(''); + + for (let i = 0; i < data.length; i++) { + hash = (hash << 5) + hash + data.charCodeAt(i); + } + + const hashString = Math.abs(hash).toString(36); // Convert to base-36 string + return hashString; +}