Skip to content

Commit 880bab2

Browse files
Merge pull request #52 from alessandrofajr/dev
Feature de link e citação do dia
2 parents a11fd29 + 7590dcf commit 880bab2

File tree

9 files changed

+1043
-41
lines changed

9 files changed

+1043
-41
lines changed

eleventy.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ module.exports = function(eleventyConfig) {
99

1010
eleventyConfig.addPassthroughCopy("src/favicon.svg");
1111

12+
eleventyConfig.addPassthroughCopy("src/js");
13+
1214
eleventyConfig.addPassthroughCopy("src/img");
1315

1416
eleventyConfig.addPassthroughCopy("src/CNAME");

src/_data/links.json

Lines changed: 425 additions & 1 deletion
Large diffs are not rendered by default.

src/_data/quotes.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,56 @@
11
[
2+
{
3+
"id": "22",
4+
"title": "Tu Não Te Moves de Ti - Hilda Hilst",
5+
"quote": "[...] é dor, Maria, como tudo o que acontece nos adentros. Não sentes então, numa soma final, que é mais dor do que alegria o existir?",
6+
"author": "Hilda Hilst",
7+
"source": "Tu Não Te Moves de Ti (p. 402, livro de coletâneas)",
8+
"thoughts": null,
9+
"date": "2025-11-07",
10+
"language": "pt-br",
11+
"tags": [
12+
"existência"
13+
]
14+
},
15+
{
16+
"id": "21",
17+
"title": "1Q84 - Haruki Murakami",
18+
"quote": "Tengo despertou em seu interior um tipo de memória ancestral: a lua sempre fora amiga dos homens, desde antes da descoberta do fogo, das ferramentas e da linguagem. Ela sempre iluminou a escuridão do mundo com sua luz natural, amenizando o medo dos homens. As fases da lua proporcionaram a noção de tempo. A gratidão por essa misericordiosa compaixão devia estar fortemente impressa nos genes da humanidade, como uma cálida memória coletiva, a despeito de, hoje em dia, a escuridão ter sido expulsa em grande parte do mundo.",
19+
"author": "Haruki Murakami",
20+
"source": "1Q84 (p. 292, livro 2)",
21+
"thoughts": "<p>“[...] À medida que essas dúvidas aumentavam, Tengo resolveu se distanciar da matemática. Assim, a floresta de histórias passou a exercer uma atração ainda mais intensa sobre ele. É claro que ler histórias também não deixava de ser um tipo de fuga, pois, ao fechar preciso voltar ao mundo real. No entanto, as páginas do livro, era certo dia. Tengo percebeu que o retorno do mundo das histórias não era tão frustrante quanto o do mundo da matemática. Mas por quê? Pensou seriamente e chegou à seguinte conclusão: na floresta de histórias, independentemente de elucidar as intrínsecas relações entre os fatos, não há como obter uma resposta clara. Esse era o ponto que a distinguia da matemática. A finalidade da narrativa, em linhas gerais, é desenvolver um problema colocando-o sob outro parâmetro. Conforme a maneira de se expressar e o sentido dessa colocação, a própria história é que vai sugerir algumas respostas possíveis. Era com essas sugestões em mãos que Tengo voltava para o mundo real. Era como se trouxesse consigo um nsigo pedaço de papel com palavras mágicas a serem decifradas. Às vezes essas palavras eram incoerentes e, de imediato, não serviam para nada. Mas, mesmo assim, elas continham uma possibilidade. A de um dia conseguir desvendá-las. Era justamente essa possibilidade, vinda de seu âmago, que lhe aquecia o coração. (p. 251, livro 1)”</p><br><p>“No entanto, em meio a essa tranquilidade, havia uma pequena mudança. Uma mudança boa. Enquanto escrevia seu romance, Tengo percebeu que uma nova fonte passara a existir dentro dele. Não que fosse uma fonte de águas abundantes, mas uma bem pequena e modesta, a correr entre os rochedos. Apesar de o volume de água ainda ser pequeno, ela brotava pouco a pouco, ininterruptamente. Tengo não precisava ter pressa nem se afobar. Era só esperar a água acumular nos espaços entre as rochas. Uma vez acumulada, bastava recolhê-la com as mãos e, em seguida, se sentar em frente à mesa e transformá-la em texto. Era assim, de modo espontâneo, que sua história se desenvolvia. (p. 278, livro 1)”</p><br><p>“Eram muitas as perguntas. Se Tengo não estava enganado, foi Tchekhov quem disse: 'O escritor não é uma pessoa que solu- ciona problemas. É uma pessoa que os propõe.' Sábias palavras que Tchekhov soube aplicar não somente a suas obras, mas também a sua vida. (p. 368, livro 1)”</p><br><p>“A maioria das pessoas não busca a comprovação da verdade. A verdade quase sempre traz consigo uma intensa dor, como você mesma acabou de dizer. Elas não buscam a verdade que vem acompanhada da dor. O que as pessoas querem é uma história bonita e agradável, que as faça enxergar um sentido em suas vidas. E por isso que existem as religiões. (p. 175, livro 2)”</p><br><p>“As pessoas inocentemente passavam diante de sua lente telescópica. A despeito das idades e das fases da vida, todos pareciam cansados e insatisfeitos com a rotina que levavam. Desejos desbotados, ambições esquecidas, sentimentos desgastados e um espaço vazio onde imperava o sentimento de conformismo e apatia. Os rostos eram sombrios e os passos pesados, como se estivessem sob o efeito de anestesia, aplicada duas horas antes, para a extração de um dente. Podia ser uma interpretação equivocada de Ushikawa. Alguns, na verdade, podem levar uma vida plena e feliz. Ao abrir a porta do apartamento, podemos encontrar um paraíso pessoal surpreendente. Ou poderiam ser pessoas que optaram por uma vida simples e modesta para fugir do fisco. Isso era plausível. Mas, como meros transeuntes que atravessavam as lentes telescópicas da câmera, eles não passavam de cidadãos sem perspectiva de conquistar uma vida melhor, presos a um apartamento barato, prestes a ser demolido. (p. 208, livro 3)”</p><br><p>“Quem nunca passou por isso, jamais saberá o quanto a experiência é horrível. A dor é um conceito que não se pode generalizar. O sofrimento de cada um possui características próprias. Se me permite parafrasear Tolstoi, toda a felicidade é igual, mas cada dor é dolorosa à sua própria maneira. Mas eu não iria tão longe a ponto de afirmar que é uma questão de gosto. Você não acha? (p. 388, livro 3)”</p>",
22+
"date": "2025-11-07",
23+
"language": "pt-br",
24+
"tags": [
25+
"amor",
26+
"existência"
27+
]
28+
},
29+
{
30+
"id": "20",
31+
"title": "O amor nos tempos do cólera - Gabriel Garcia Márquez",
32+
"quote": "Acabavam de celebrar as bodas de ouro matrimoniais, e não sabiam viver um instante sequer um sem o outro, ou sem pensar um no outro, e o sabiam cada vez menos à medida que recrudescia a velhice. Nem ele nem ela podiam dizer se essa servidão recíproca se fundava no amor ou na comodidade, mas nunca se haviam feito a pergunta com a mão no peito, porque ambos tinham sempre preferido ignorar a resposta. [...] Coisa bem diferente teria sido a vida para ambos se tivessem sabido a tempo que era mais fácil contornar as grandes catástrofes matrimoniais do que as misérias minúsculas de cada dia. Mas se alguma coisa haviam aprendido juntos era que a sabedoria nos chega quando já não serve para nada.",
33+
"author": "Gabriel Garcia Márquez",
34+
"source": "O amor nos tempos do cólera (p. 39)",
35+
"thoughts": "<p>“Era ainda jovem demais para saber que a memória do coração elimina as más lembranças e enaltece as boas e que graças a esse artificio conseguimos suportar o passado. Mas quando voltou a ver do convés do navio o promontório branco do bairro colonial, os urubus imóveis nos telhados, a roupa dos pobres estendida a secar nas sacadas, compreendeu até que ponto tinha sido uma vítima fácil das burlas caritativas da saudade. (p. 136)”</p><br><p>“Ele tinha consciência de que não a amava. Casara-se porque gostava da sua altivez, sua seriedade, sua força e também por um tico de vaidade, mas enquanto ela o beijava pela primeira vez teve a certeza de que não haveria nenhum obstáculo para inventar um bom amor. Não falaram a respeito nessa primeira noite em que falaram de tudo até o amanhecer, nem falariam nunca. Mas de um modo geral, nenhum dos dois se equivocou. (p. 202)”</p><br><p>“O tio estava sentido com ele devido à maneira por que malbaratara o bom emprego de telegrafista na Vila de Leyva, mas se deixou levar por sua convicção de que os seres humanos não nascem para sempre no dia em que as mães os dão à luz, e sim que a vida os obriga outra vez e muitas vezes a se parirem a si mesmos. (p. 207)”</p>",
36+
"date": "2025-11-07",
37+
"language": "pt-br",
38+
"tags": [
39+
"amor",
40+
"existência"
41+
]
42+
},
43+
{
44+
"id": "19",
45+
"title": "Ensaio sobre a lucidez - José Saramago",
46+
"quote": "[...] O silêncio que se sucedeu a estas palavras demonstrou uma vez mais que o tempo não tem nada que ver com o que dele nos dizem os relógios, essas maquinetas feitas de rodas que não pensam e de molas que não sentem. desprovidas de um espírito que lhes permitiria imaginar que cinco insignificantes segundos escandidos, o primeiro, o segundo, o terceiro, o quarto, o quinto, haviam sido uma agónica tortura para um lado e um remanso de sublime gozo para o outro.",
47+
"author": "José Saramago",
48+
"source": "Ensaio sobre a lucidez (p. 148)",
49+
"thoughts": "Quando nascemos, quando entramos neste mundo, é como se firmássemos um pacto para toda a vida, mas pode acontecer que um dia tenhamos de nos per- guntar Quem assinou isto por mim, eu perguntei e a resposta é esse papel. (p. 302)",
50+
"date": "2025-11-07",
51+
"language": "pt-br",
52+
"tags": ["tempo"]
53+
},
254
{
355
"id": "18",
456
"title": "O Jogador - Fiódor Dostoiévski",

src/_includes/layouts/default.njk

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
<html lang="pt-br">
33
<head>
44
{% include "partials/head.njk" %}
5+
6+
<!-- Expose JSON data to client-side JavaScript -->
7+
<script>
8+
// Make quotes and links data available globally before other scripts load
9+
window.SITE_DATA = {
10+
quotes: {{ quotes | dump | safe }},
11+
links: {{ links | dump | safe }}
12+
};
13+
</script>
514
</head>
615
<body>
716
<div class="wrapper">
@@ -12,6 +21,10 @@
1221
{{ content | safe }}
1322
</main>
1423
{% include "partials/footer.njk" %}
24+
25+
<script src="/js/daily-content-manager.js"></script>
26+
<script src="/js/daily-components.js"></script>
27+
<script src="/js/homepage-init.js"></script>
1528
</body>
1629
</div>
1730
</html>

src/index.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ layout: default
33
title: alessandro feitosa jr.
44
---
55

6-
## <span class="section-title">eu costumava entrevistar pessoas para contar histórias. agora eu entrevisto dados</span>
6+
## <span class="section-title">eu costumava entrevistar pessoas para contar histórias. agora, eu entrevisto dados</span>
77

88
aqui, escrevo sobre o que tenho feito e compartilho aquilo que tem me interessado
99

@@ -15,4 +15,11 @@ aqui, escrevo sobre o que tenho feito e compartilho aquilo que tem me interessad
1515
<li>{ <a href="/about/">sobre</a> }</li>
1616
<li>{ <a href="https://github.com/alessandrofajr/" target="_blank">github</a> }</li>
1717
<li>{ <a href="https://www.linkedin.com/in/alessandrofajr/" target="_blank">linkedin</a> }</li>
18-
</ul>
18+
</ul>
19+
20+
<div class="daily-components">
21+
<div id="daily-quote-container" class="daily-component">
22+
</div>
23+
<div id="daily-link-container" class="daily-component">
24+
</div>
25+
</div>

src/js/daily-components.js

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* Daily Components - UI components for rendering daily quote and link
3+
*/
4+
5+
/**
6+
* DailyQuoteComponent - Handles rendering of daily quote
7+
*/
8+
class DailyQuoteComponent {
9+
constructor(container, contentManager) {
10+
this.container = container;
11+
this.contentManager = contentManager;
12+
}
13+
14+
/**
15+
* Render the daily quote component
16+
*/
17+
render() {
18+
try {
19+
const quote = this.contentManager.getDailyQuote();
20+
21+
if (!quote) {
22+
this.renderFallback('Vixe... Algo deu errado.');
23+
return;
24+
}
25+
26+
const html = this.generateQuoteHTML(quote);
27+
this.container.innerHTML = html;
28+
} catch (error) {
29+
console.error('Error rendering daily quote:', error);
30+
this.renderFallback('Erro ao carregar citação');
31+
}
32+
}
33+
34+
/**
35+
* Generate HTML for quote display
36+
* @param {Object} quote - Quote object from data
37+
* @returns {string} HTML string for quote
38+
*/
39+
generateQuoteHTML(quote) {
40+
const quoteUrl = this.generateQuoteURL(quote);
41+
const quoteText = this.escapeHtml(quote.quote || 'Citação não disponível');
42+
const author = quote.author ? this.escapeHtml(quote.author) : '';
43+
44+
return `
45+
<div class="daily-quote">
46+
<div class="daily-quote__label">citação do dia</div>
47+
<p class="daily-quote__text">
48+
<a href="${quoteUrl}" class="daily-quote__link">${quoteText}</a>
49+
</p>
50+
${author ? `<p class="daily-quote__author">— ${author}</p>` : ''}
51+
</div>
52+
`;
53+
}
54+
55+
/**
56+
* Generate URL for quote page
57+
* @param {Object} quote - Quote object
58+
* @returns {string} URL path to quote page
59+
*/
60+
generateQuoteURL(quote) {
61+
if (!quote.title) return '/quotes/';
62+
63+
// Improved slugify logic to match 11ty behavior
64+
const slug = quote.title
65+
.toLowerCase()
66+
// Normalize accented characters
67+
.normalize('NFD')
68+
.replace(/[\u0300-\u036f]/g, '') // Remove diacritics
69+
// Replace em dashes and en dashes with regular hyphens
70+
.replace(/[]/g, '-')
71+
// Remove special characters but keep letters, numbers, spaces, and hyphens
72+
.replace(/[^a-z0-9\s-]/g, '')
73+
// Replace multiple spaces/hyphens with single hyphen
74+
.replace(/[\s-]+/g, '-')
75+
// Remove leading/trailing hyphens
76+
.replace(/^-+|-+$/g, '');
77+
78+
return `/quotes/${slug}`;
79+
}
80+
81+
/**
82+
* Render fallback message
83+
* @param {string} message - Fallback message to display
84+
*/
85+
renderFallback(message) {
86+
this.container.innerHTML = `
87+
<div class="daily-quote daily-quote--fallback">
88+
<div class="daily-quote__label">citação do dia</div>
89+
<p class="daily-quote__fallback">${this.escapeHtml(message)}</p>
90+
</div>
91+
`;
92+
}
93+
94+
/**
95+
* Escape HTML to prevent XSS
96+
* @param {string} text - Text to escape
97+
* @returns {string} Escaped text
98+
*/
99+
escapeHtml(text) {
100+
const div = document.createElement('div');
101+
div.textContent = text;
102+
return div.innerHTML;
103+
}
104+
}
105+
106+
/**
107+
* DailyLinkComponent - Handles rendering of daily link
108+
*/
109+
class DailyLinkComponent {
110+
constructor(container, contentManager) {
111+
this.container = container;
112+
this.contentManager = contentManager;
113+
}
114+
115+
/**
116+
* Render the daily link component
117+
*/
118+
render() {
119+
try {
120+
const link = this.contentManager.getDailyLink();
121+
122+
if (!link) {
123+
this.renderFallback('Vixe... Algo deu errado.');
124+
return;
125+
}
126+
127+
const html = this.generateLinkHTML(link);
128+
this.container.innerHTML = html;
129+
} catch (error) {
130+
console.error('Error rendering daily link:', error);
131+
this.renderFallback('Erro ao carregar link');
132+
}
133+
}
134+
135+
/**
136+
* Generate HTML for link display
137+
* @param {Object} link - Link object from data
138+
* @returns {string} HTML string for link
139+
*/
140+
generateLinkHTML(link) {
141+
const title = this.escapeHtml(link.title || 'Sem título');
142+
const url = this.escapeHtml(link.link || '#');
143+
const notes = link.notes ? this.escapeHtml(link.notes) : '';
144+
145+
return `
146+
<div class="daily-link">
147+
<div class="daily-link__label">link do dia</div>
148+
<h3 class="daily-link__title">
149+
<a href="${url}"
150+
class="daily-link__link"
151+
target="_blank"
152+
rel="noopener noreferrer">${title}</a>
153+
</h3>
154+
${notes ? `<p class="daily-link__notes">${notes}</p>` : ''}
155+
</div>
156+
`;
157+
}
158+
159+
/**
160+
* Render fallback message
161+
* @param {string} message - Fallback message to display
162+
*/
163+
renderFallback(message) {
164+
this.container.innerHTML = `
165+
<div class="daily-link daily-link--fallback">
166+
<div class="daily-link__label">link do dia</div>
167+
<p class="daily-link__fallback">${this.escapeHtml(message)}</p>
168+
</div>
169+
`;
170+
}
171+
172+
/**
173+
* Escape HTML to prevent XSS
174+
* @param {string} text - Text to escape
175+
* @returns {string} Escaped text
176+
*/
177+
escapeHtml(text) {
178+
const div = document.createElement('div');
179+
div.textContent = text;
180+
return div.innerHTML;
181+
}
182+
}
183+
184+
// Export for both CommonJS and ES modules
185+
if (typeof module !== 'undefined' && module.exports) {
186+
module.exports = { DailyQuoteComponent, DailyLinkComponent };
187+
} else if (typeof window !== 'undefined') {
188+
window.DailyQuoteComponent = DailyQuoteComponent;
189+
window.DailyLinkComponent = DailyLinkComponent;
190+
}

src/js/daily-content-manager.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* DailyContentManager - Core class for managing daily content selection
3+
* Handles Brazilian timezone calculations and deterministic random selection
4+
*/
5+
class DailyContentManager {
6+
constructor(quotesData, linksData) {
7+
this.quotesData = quotesData || [];
8+
this.linksData = linksData || [];
9+
}
10+
11+
/**
12+
* Get daily quote for the current Brazilian date
13+
* @returns {Object|null} Selected quote object or null if no data
14+
*/
15+
getDailyQuote() {
16+
const brazilianDate = this.getBrazilianDate();
17+
return this.selectRandomItem(this.quotesData, this.generateSeed(brazilianDate));
18+
}
19+
20+
/**
21+
* Get daily link for the current Brazilian date
22+
* @returns {Object|null} Selected link object or null if no data
23+
*/
24+
getDailyLink() {
25+
const brazilianDate = this.getBrazilianDate();
26+
return this.selectRandomItem(this.linksData, this.generateSeed(brazilianDate));
27+
}
28+
29+
/**
30+
* Calculate current date in Brazilian timezone (GMT-3)
31+
* @returns {Date} Date object adjusted for Brazilian timezone
32+
*/
33+
getBrazilianDate() {
34+
const now = new Date();
35+
const utc = now.getTime() + (now.getTimezoneOffset() * 60000);
36+
const brazilianTime = new Date(utc + (-3 * 3600000)); // GMT-3
37+
return brazilianTime;
38+
}
39+
40+
/**
41+
* Generate deterministic seed based on date
42+
* @param {Date} date - Date to generate seed from
43+
* @returns {number} Numeric seed for random selection
44+
*/
45+
generateSeed(date) {
46+
const dateString = date.toISOString().split('T')[0]; // YYYY-MM-DD format
47+
let hash = 0;
48+
for (let i = 0; i < dateString.length; i++) {
49+
const char = dateString.charCodeAt(i);
50+
hash = ((hash << 5) - hash) + char;
51+
hash = hash & hash; // Convert to 32-bit integer
52+
}
53+
return Math.abs(hash);
54+
}
55+
56+
/**
57+
* Select random item from array using deterministic seed
58+
* @param {Array} items - Array of items to select from
59+
* @param {number} seed - Seed for deterministic selection
60+
* @returns {Object|null} Selected item or null if array is empty
61+
*/
62+
selectRandomItem(items, seed) {
63+
if (!Array.isArray(items) || items.length === 0) {
64+
return null;
65+
}
66+
67+
const index = seed % items.length;
68+
return items[index];
69+
}
70+
}
71+
72+
// Export for both CommonJS and ES modules
73+
if (typeof module !== 'undefined' && module.exports) {
74+
module.exports = DailyContentManager;
75+
} else if (typeof window !== 'undefined') {
76+
window.DailyContentManager = DailyContentManager;
77+
}

0 commit comments

Comments
 (0)