sexyjekyll-theme 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +82 -0
- data/LICENSE +21 -0
- data/README.de.md +276 -0
- data/README.es.md +276 -0
- data/README.fr.md +276 -0
- data/README.it.md +219 -0
- data/README.md +276 -0
- data/_includes/critical-css.html +4 -0
- data/_includes/footer.html +81 -0
- data/_includes/head.html +88 -0
- data/_includes/nav.html +21 -0
- data/_includes/related-posts.html +75 -0
- data/_includes/social-icon.html +98 -0
- data/_includes/structured-data-article.html +55 -0
- data/_includes/structured-data-breadcrumb.html +28 -0
- data/_includes/structured-data-person.html +82 -0
- data/_includes/structured-data-website.html +23 -0
- data/_layouts/blog.html +101 -0
- data/_layouts/category.html +66 -0
- data/_layouts/contact.html +26 -0
- data/_layouts/default.html +13 -0
- data/_layouts/home.html +30 -0
- data/_layouts/llms.txt +34 -0
- data/_layouts/post.html +99 -0
- data/_plugins/auto_related_posts.rb +153 -0
- data/_plugins/category_generator.rb +27 -0
- data/_plugins/llms_txt_generator.rb +45 -0
- data/_plugins/localized_date.rb +52 -0
- data/assets/bg.jpeg +0 -0
- data/assets/bg.webp +0 -0
- data/assets/debug/blocco.png +0 -0
- data/assets/debug/categorie.jpeg +0 -0
- data/assets/debug/categorie.png +0 -0
- data/assets/debug/codice.png +0 -0
- data/assets/debug/contrasto.jpeg +0 -0
- data/assets/debug/dipendenze.png +0 -0
- data/assets/debug/h1.png +0 -0
- data/assets/debug/pagespeed.png +0 -0
- data/assets/debug/ricerca.png +0 -0
- data/assets/debug/richieste.png +0 -0
- data/assets/favicon/android-icon-144x144.png +0 -0
- data/assets/favicon/android-icon-192x192.png +0 -0
- data/assets/favicon/android-icon-36x36.png +0 -0
- data/assets/favicon/android-icon-48x48.png +0 -0
- data/assets/favicon/android-icon-72x72.png +0 -0
- data/assets/favicon/android-icon-96x96.png +0 -0
- data/assets/favicon/apple-icon-114x114.png +0 -0
- data/assets/favicon/apple-icon-120x120.png +0 -0
- data/assets/favicon/apple-icon-144x144.png +0 -0
- data/assets/favicon/apple-icon-152x152.png +0 -0
- data/assets/favicon/apple-icon-180x180.png +0 -0
- data/assets/favicon/apple-icon-57x57.png +0 -0
- data/assets/favicon/apple-icon-60x60.png +0 -0
- data/assets/favicon/apple-icon-72x72.png +0 -0
- data/assets/favicon/apple-icon-76x76.png +0 -0
- data/assets/favicon/apple-icon-precomposed.png +0 -0
- data/assets/favicon/apple-icon.png +0 -0
- data/assets/favicon/favicon-16x16.png +0 -0
- data/assets/favicon/favicon-32x32.png +0 -0
- data/assets/favicon/favicon-96x96.png +0 -0
- data/assets/favicon/favicon.ico +0 -0
- data/assets/favicon/ms-icon-144x144.png +0 -0
- data/assets/favicon/ms-icon-150x150.png +0 -0
- data/assets/favicon/ms-icon-310x310.png +0 -0
- data/assets/favicon/ms-icon-70x70.png +0 -0
- data/assets/images/aiact.jpeg +0 -0
- data/assets/images/aiethics.jpeg +0 -0
- data/assets/images/green.jpeg +0 -0
- data/assets/images/jekyll.webp +0 -0
- data/assets/images/parenting.jpeg +0 -0
- data/assets/images/seo-generativo.jpeg +0 -0
- data/assets/images/upskilling-ai.jpeg +0 -0
- data/assets/pic.jpeg +0 -0
- data/assets/screens/screen.jpeg +0 -0
- data/assets/screens/screen2.jpeg +0 -0
- data/css/animations.css +404 -0
- data/css/style.css +2250 -0
- data/css/syntax-dark.css +157 -0
- data/css/syntax-light.css +157 -0
- data/js/main.js +706 -0
- data/js/simple-jekyll-search.min.js +6 -0
- metadata +254 -0
data/js/main.js
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
// =======================
|
|
2
|
+
// Main JavaScript
|
|
3
|
+
// =======================
|
|
4
|
+
|
|
5
|
+
// Wait for DOM to be fully loaded
|
|
6
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
7
|
+
initDarkMode();
|
|
8
|
+
initSmoothScroll();
|
|
9
|
+
initNavigation();
|
|
10
|
+
initAnimatedBackground();
|
|
11
|
+
initSearch();
|
|
12
|
+
initReadingProgress();
|
|
13
|
+
initExternalLinks();
|
|
14
|
+
initPageAnimations();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// =======================
|
|
18
|
+
// Dark Mode (Always Active)
|
|
19
|
+
// =======================
|
|
20
|
+
|
|
21
|
+
function initDarkMode() {
|
|
22
|
+
// Set dark mode permanently
|
|
23
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
24
|
+
|
|
25
|
+
// Force dark syntax highlighting
|
|
26
|
+
const darkLink = document.querySelector('link[href*="syntax-dark"]');
|
|
27
|
+
const lightLink = document.querySelector('link[href*="syntax-light"]');
|
|
28
|
+
|
|
29
|
+
if (darkLink && lightLink) {
|
|
30
|
+
darkLink.removeAttribute('disabled');
|
|
31
|
+
darkLink.media = 'all';
|
|
32
|
+
lightLink.setAttribute('disabled', 'disabled');
|
|
33
|
+
lightLink.media = 'not all';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// =======================
|
|
38
|
+
// Smooth Scroll
|
|
39
|
+
// =======================
|
|
40
|
+
|
|
41
|
+
function initSmoothScroll() {
|
|
42
|
+
// Handle smooth scrolling for anchor links
|
|
43
|
+
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
44
|
+
anchor.addEventListener('click', function (e) {
|
|
45
|
+
const href = this.getAttribute('href');
|
|
46
|
+
|
|
47
|
+
// Don't prevent default if href is just "#"
|
|
48
|
+
if (href === '#') return;
|
|
49
|
+
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
|
|
52
|
+
const target = document.querySelector(href);
|
|
53
|
+
if (target) {
|
|
54
|
+
const headerOffset = 80; // Account for fixed header
|
|
55
|
+
const elementPosition = target.getBoundingClientRect().top;
|
|
56
|
+
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
|
|
57
|
+
|
|
58
|
+
window.scrollTo({
|
|
59
|
+
top: offsetPosition,
|
|
60
|
+
behavior: 'smooth'
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// =======================
|
|
68
|
+
// Navigation (Always Visible)
|
|
69
|
+
// =======================
|
|
70
|
+
|
|
71
|
+
function initNavigation() {
|
|
72
|
+
// Active nav link highlighting
|
|
73
|
+
highlightActiveSection();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// =======================
|
|
77
|
+
// Active Section Highlighting
|
|
78
|
+
// =======================
|
|
79
|
+
|
|
80
|
+
function highlightActiveSection() {
|
|
81
|
+
const sections = document.querySelectorAll('section[id]');
|
|
82
|
+
const navLinks = document.querySelectorAll('.nav-link');
|
|
83
|
+
|
|
84
|
+
if (!('IntersectionObserver' in window)) return;
|
|
85
|
+
|
|
86
|
+
const observerOptions = {
|
|
87
|
+
threshold: 0.3,
|
|
88
|
+
rootMargin: '-80px 0px -70% 0px'
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const observer = new IntersectionObserver((entries) => {
|
|
92
|
+
entries.forEach(entry => {
|
|
93
|
+
if (entry.isIntersecting) {
|
|
94
|
+
const id = entry.target.getAttribute('id');
|
|
95
|
+
|
|
96
|
+
// Remove active class from all links
|
|
97
|
+
navLinks.forEach(link => {
|
|
98
|
+
link.classList.remove('active');
|
|
99
|
+
if (link.getAttribute('href') === `#${id}`) {
|
|
100
|
+
link.classList.add('active');
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}, observerOptions);
|
|
106
|
+
|
|
107
|
+
sections.forEach(section => {
|
|
108
|
+
observer.observe(section);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// =======================
|
|
113
|
+
// Utility Functions
|
|
114
|
+
// =======================
|
|
115
|
+
|
|
116
|
+
// Debounce function for performance optimization
|
|
117
|
+
function debounce(func, wait) {
|
|
118
|
+
let timeout;
|
|
119
|
+
return function executedFunction(...args) {
|
|
120
|
+
const later = () => {
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
func(...args);
|
|
123
|
+
};
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
timeout = setTimeout(later, wait);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Throttle function for scroll events
|
|
130
|
+
function throttle(func, limit) {
|
|
131
|
+
let inThrottle;
|
|
132
|
+
return function(...args) {
|
|
133
|
+
if (!inThrottle) {
|
|
134
|
+
func.apply(this, args);
|
|
135
|
+
inThrottle = true;
|
|
136
|
+
setTimeout(() => inThrottle = false, limit);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// =======================
|
|
142
|
+
// Performance Monitoring
|
|
143
|
+
// =======================
|
|
144
|
+
|
|
145
|
+
// Log performance metrics (optional, for development)
|
|
146
|
+
if (window.performance && console.debug) {
|
|
147
|
+
window.addEventListener('load', () => {
|
|
148
|
+
const perfData = window.performance.timing;
|
|
149
|
+
const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
|
|
150
|
+
const connectTime = perfData.responseEnd - perfData.requestStart;
|
|
151
|
+
const renderTime = perfData.domComplete - perfData.domLoading;
|
|
152
|
+
|
|
153
|
+
console.debug('Page Load Time:', pageLoadTime + 'ms');
|
|
154
|
+
console.debug('Connect Time:', connectTime + 'ms');
|
|
155
|
+
console.debug('Render Time:', renderTime + 'ms');
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// =======================
|
|
160
|
+
// Error Handling
|
|
161
|
+
// =======================
|
|
162
|
+
|
|
163
|
+
// Global error handler
|
|
164
|
+
window.addEventListener('error', (event) => {
|
|
165
|
+
console.error('Global error:', event.error);
|
|
166
|
+
// You can add error reporting service here (e.g., Sentry)
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Handle unhandled promise rejections
|
|
170
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
171
|
+
console.error('Unhandled promise rejection:', event.reason);
|
|
172
|
+
// You can add error reporting service here
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// =======================
|
|
176
|
+
// Animated Background
|
|
177
|
+
// =======================
|
|
178
|
+
|
|
179
|
+
function initAnimatedBackground() {
|
|
180
|
+
const blobs = document.querySelectorAll('.gradient-blob');
|
|
181
|
+
|
|
182
|
+
// Add mouse move parallax effect
|
|
183
|
+
document.addEventListener('mousemove', (e) => {
|
|
184
|
+
const mouseX = e.clientX / window.innerWidth;
|
|
185
|
+
const mouseY = e.clientY / window.innerHeight;
|
|
186
|
+
|
|
187
|
+
blobs.forEach((blob, index) => {
|
|
188
|
+
const speed = (index + 1) * 0.5;
|
|
189
|
+
const x = (mouseX - 0.5) * speed * 50;
|
|
190
|
+
const y = (mouseY - 0.5) * speed * 50;
|
|
191
|
+
|
|
192
|
+
blob.style.transform = `translate(${x}px, ${y}px)`;
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Add random color shift over time
|
|
197
|
+
function shiftColors() {
|
|
198
|
+
const colors = [
|
|
199
|
+
{ name: 'purple', value: '#a855f7' },
|
|
200
|
+
{ name: 'pink', value: '#ec4899' },
|
|
201
|
+
{ name: 'blue', value: '#3b82f6' },
|
|
202
|
+
{ name: 'cyan', value: '#06b6d4' },
|
|
203
|
+
{ name: 'orange', value: '#f97316' },
|
|
204
|
+
{ name: 'yellow', value: '#facc15' }
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
blobs.forEach((blob, index) => {
|
|
208
|
+
const randomColor = colors[Math.floor(Math.random() * colors.length)];
|
|
209
|
+
const currentBg = blob.style.background || '';
|
|
210
|
+
|
|
211
|
+
// Subtle color changes without jarring transitions
|
|
212
|
+
if (Math.random() > 0.7) {
|
|
213
|
+
blob.style.background = `radial-gradient(circle, ${randomColor.value} 0%, transparent 70%)`;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Shift colors every 10 seconds
|
|
219
|
+
setInterval(shiftColors, 10000);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// =======================
|
|
223
|
+
// Search Functionality
|
|
224
|
+
// =======================
|
|
225
|
+
|
|
226
|
+
function initSearch() {
|
|
227
|
+
const searchInput = document.getElementById('search-input');
|
|
228
|
+
const searchClear = document.getElementById('search-clear');
|
|
229
|
+
const searchResults = document.getElementById('search-results');
|
|
230
|
+
const searchResultsCount = document.getElementById('search-results-count');
|
|
231
|
+
const regularPosts = document.getElementById('regular-posts');
|
|
232
|
+
const pagination = document.getElementById('pagination');
|
|
233
|
+
|
|
234
|
+
// Only initialize if we're on a page with search
|
|
235
|
+
if (!searchInput) return;
|
|
236
|
+
|
|
237
|
+
// Load Simple Jekyll Search library
|
|
238
|
+
const script = document.createElement('script');
|
|
239
|
+
script.src = '/js/simple-jekyll-search.min.js';
|
|
240
|
+
script.onload = () => {
|
|
241
|
+
// Initialize Simple Jekyll Search
|
|
242
|
+
window.simpleJekyllSearch = SimpleJekyllSearch({
|
|
243
|
+
searchInput: searchInput,
|
|
244
|
+
resultsContainer: searchResults,
|
|
245
|
+
json: '/search.json',
|
|
246
|
+
searchResultTemplate: `
|
|
247
|
+
<article class="blog-post-card">
|
|
248
|
+
<div class="post-card-content">
|
|
249
|
+
<time class="post-date" datetime="{date}">{date}</time>
|
|
250
|
+
<h2 class="post-card-title">
|
|
251
|
+
<a href="{url}">{title}</a>
|
|
252
|
+
</h2>
|
|
253
|
+
<p class="post-card-excerpt">{excerpt}</p>
|
|
254
|
+
<div class="post-card-footer">
|
|
255
|
+
<div class="post-categories">
|
|
256
|
+
<span class="post-category">{category}</span>
|
|
257
|
+
</div>
|
|
258
|
+
<a href="{url}" class="read-more">Leggi →</a>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</article>
|
|
262
|
+
`,
|
|
263
|
+
noResultsText: `
|
|
264
|
+
<div class="search-no-results">
|
|
265
|
+
<h3>Nessun risultato trovato</h3>
|
|
266
|
+
<p>Prova con parole chiave diverse o sfoglia tutti gli articoli qui sotto.</p>
|
|
267
|
+
</div>
|
|
268
|
+
`,
|
|
269
|
+
limit: 20,
|
|
270
|
+
fuzzy: false,
|
|
271
|
+
exclude: []
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
document.head.appendChild(script);
|
|
275
|
+
|
|
276
|
+
// Handle search input
|
|
277
|
+
searchInput.addEventListener('input', debounce((e) => {
|
|
278
|
+
const query = e.target.value.trim();
|
|
279
|
+
|
|
280
|
+
if (query.length > 0) {
|
|
281
|
+
// Show clear button
|
|
282
|
+
searchClear.style.display = 'flex';
|
|
283
|
+
|
|
284
|
+
// Show search results, hide regular posts and pagination
|
|
285
|
+
searchResults.style.display = 'grid';
|
|
286
|
+
if (regularPosts) regularPosts.style.display = 'none';
|
|
287
|
+
if (pagination) pagination.style.display = 'none';
|
|
288
|
+
|
|
289
|
+
// Count results after a short delay to let Simple Jekyll Search finish
|
|
290
|
+
setTimeout(() => {
|
|
291
|
+
const resultCount = searchResults.children.length;
|
|
292
|
+
if (resultCount > 0 && !searchResults.querySelector('.search-no-results')) {
|
|
293
|
+
searchResultsCount.style.display = 'block';
|
|
294
|
+
searchResultsCount.innerHTML = `Trovat${resultCount !== 1 ? 'i' : 'o'} <strong>${resultCount}</strong> risultat${resultCount !== 1 ? 'i' : 'o'} per "<strong>${escapeHtml(query)}</strong>"`;
|
|
295
|
+
} else {
|
|
296
|
+
searchResultsCount.style.display = 'none';
|
|
297
|
+
}
|
|
298
|
+
}, 100);
|
|
299
|
+
} else {
|
|
300
|
+
clearSearch();
|
|
301
|
+
}
|
|
302
|
+
}, 300));
|
|
303
|
+
|
|
304
|
+
// Handle clear button
|
|
305
|
+
searchClear.addEventListener('click', () => {
|
|
306
|
+
searchInput.value = '';
|
|
307
|
+
searchInput.focus();
|
|
308
|
+
clearSearch();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Clear search on Escape key
|
|
312
|
+
searchInput.addEventListener('keydown', (e) => {
|
|
313
|
+
if (e.key === 'Escape') {
|
|
314
|
+
searchInput.value = '';
|
|
315
|
+
clearSearch();
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
function clearSearch() {
|
|
320
|
+
searchClear.style.display = 'none';
|
|
321
|
+
searchResults.style.display = 'none';
|
|
322
|
+
searchResultsCount.style.display = 'none';
|
|
323
|
+
if (regularPosts) regularPosts.style.display = 'grid';
|
|
324
|
+
if (pagination) pagination.style.display = 'flex';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Escape HTML to prevent XSS
|
|
328
|
+
function escapeHtml(text) {
|
|
329
|
+
const map = {
|
|
330
|
+
'&': '&',
|
|
331
|
+
'<': '<',
|
|
332
|
+
'>': '>',
|
|
333
|
+
'"': '"',
|
|
334
|
+
"'": '''
|
|
335
|
+
};
|
|
336
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// =======================
|
|
341
|
+
// Reading Progress Bar
|
|
342
|
+
// =======================
|
|
343
|
+
|
|
344
|
+
function initReadingProgress() {
|
|
345
|
+
const progressBar = document.getElementById('reading-progress-bar');
|
|
346
|
+
const progressContainer = progressBar?.parentElement;
|
|
347
|
+
|
|
348
|
+
// Only initialize on blog post pages
|
|
349
|
+
if (!progressBar || !progressContainer) return;
|
|
350
|
+
|
|
351
|
+
// Get the main content element
|
|
352
|
+
const mainContent = document.querySelector('.post-content');
|
|
353
|
+
if (!mainContent) return;
|
|
354
|
+
|
|
355
|
+
// Function to calculate and update progress
|
|
356
|
+
function updateProgress() {
|
|
357
|
+
// Get the bounding rectangles
|
|
358
|
+
const contentRect = mainContent.getBoundingClientRect();
|
|
359
|
+
const windowHeight = window.innerHeight;
|
|
360
|
+
|
|
361
|
+
// Calculate the start and end points for reading
|
|
362
|
+
// Start when content top reaches viewport top
|
|
363
|
+
const startPoint = contentRect.top + window.scrollY - windowHeight * 0.2;
|
|
364
|
+
// End when content bottom reaches viewport bottom
|
|
365
|
+
const endPoint = contentRect.bottom + window.scrollY - windowHeight * 0.8;
|
|
366
|
+
|
|
367
|
+
// Calculate total scrollable distance
|
|
368
|
+
const totalDistance = endPoint - startPoint;
|
|
369
|
+
|
|
370
|
+
// Current scroll position relative to start
|
|
371
|
+
const currentScroll = window.scrollY - startPoint;
|
|
372
|
+
|
|
373
|
+
// Calculate percentage (0-100)
|
|
374
|
+
let percentage = (currentScroll / totalDistance) * 100;
|
|
375
|
+
|
|
376
|
+
// Clamp between 0 and 100
|
|
377
|
+
percentage = Math.max(0, Math.min(100, percentage));
|
|
378
|
+
|
|
379
|
+
// Update the progress bar width
|
|
380
|
+
progressBar.style.width = `${percentage}%`;
|
|
381
|
+
|
|
382
|
+
// Show progress bar when user starts scrolling
|
|
383
|
+
if (window.scrollY > 100) {
|
|
384
|
+
progressContainer.classList.add('visible');
|
|
385
|
+
} else {
|
|
386
|
+
progressContainer.classList.remove('visible');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Throttled scroll handler for better performance
|
|
391
|
+
let ticking = false;
|
|
392
|
+
function handleScroll() {
|
|
393
|
+
if (!ticking) {
|
|
394
|
+
window.requestAnimationFrame(() => {
|
|
395
|
+
updateProgress();
|
|
396
|
+
ticking = false;
|
|
397
|
+
});
|
|
398
|
+
ticking = true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Initial calculation
|
|
403
|
+
updateProgress();
|
|
404
|
+
|
|
405
|
+
// Update on scroll with throttling
|
|
406
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
407
|
+
|
|
408
|
+
// Update on resize (in case content reflows)
|
|
409
|
+
window.addEventListener('resize', debounce(() => {
|
|
410
|
+
updateProgress();
|
|
411
|
+
}, 250));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// =======================
|
|
415
|
+
// Table of Contents
|
|
416
|
+
// =======================
|
|
417
|
+
|
|
418
|
+
function initTableOfContents() {
|
|
419
|
+
const tocContainer = document.getElementById('toc-container');
|
|
420
|
+
const tocList = document.getElementById('toc-list');
|
|
421
|
+
const tocEmpty = document.getElementById('toc-empty');
|
|
422
|
+
const tocToggle = document.getElementById('toc-toggle');
|
|
423
|
+
|
|
424
|
+
// Only initialize if TOC container exists
|
|
425
|
+
if (!tocContainer || !tocList) return;
|
|
426
|
+
|
|
427
|
+
// Get all H2 and H3 headings from post content
|
|
428
|
+
const content = document.querySelector('.post-content');
|
|
429
|
+
if (!content) return;
|
|
430
|
+
|
|
431
|
+
const headings = content.querySelectorAll('h2, h3');
|
|
432
|
+
|
|
433
|
+
// If no headings found, show empty message
|
|
434
|
+
if (headings.length === 0) {
|
|
435
|
+
tocEmpty.style.display = 'block';
|
|
436
|
+
document.getElementById('toc-nav').style.display = 'none';
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Generate TOC items
|
|
441
|
+
headings.forEach((heading, index) => {
|
|
442
|
+
// Create unique ID if heading doesn't have one
|
|
443
|
+
if (!heading.id) {
|
|
444
|
+
heading.id = `heading-${index}-${slugify(heading.textContent)}`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Create TOC item
|
|
448
|
+
const tocItem = document.createElement('li');
|
|
449
|
+
tocItem.className = `toc-item toc-${heading.tagName.toLowerCase()}`;
|
|
450
|
+
|
|
451
|
+
const tocLink = document.createElement('a');
|
|
452
|
+
tocLink.className = 'toc-link';
|
|
453
|
+
tocLink.href = `#${heading.id}`;
|
|
454
|
+
tocLink.textContent = heading.textContent;
|
|
455
|
+
tocLink.setAttribute('data-heading-id', heading.id);
|
|
456
|
+
|
|
457
|
+
// Smooth scroll on click
|
|
458
|
+
tocLink.addEventListener('click', (e) => {
|
|
459
|
+
e.preventDefault();
|
|
460
|
+
const target = document.getElementById(heading.id);
|
|
461
|
+
if (target) {
|
|
462
|
+
const headerOffset = 100;
|
|
463
|
+
const elementPosition = target.getBoundingClientRect().top;
|
|
464
|
+
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
|
|
465
|
+
|
|
466
|
+
window.scrollTo({
|
|
467
|
+
top: offsetPosition,
|
|
468
|
+
behavior: 'smooth'
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Update URL hash without jumping
|
|
472
|
+
history.pushState(null, null, `#${heading.id}`);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
tocItem.appendChild(tocLink);
|
|
477
|
+
tocList.appendChild(tocItem);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Handle toggle button
|
|
481
|
+
if (tocToggle) {
|
|
482
|
+
tocToggle.addEventListener('click', () => {
|
|
483
|
+
tocContainer.classList.toggle('collapsed');
|
|
484
|
+
const isCollapsed = tocContainer.classList.contains('collapsed');
|
|
485
|
+
tocToggle.setAttribute('aria-expanded', !isCollapsed);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Highlight active section on scroll
|
|
490
|
+
const observerOptions = {
|
|
491
|
+
rootMargin: '-100px 0px -66%',
|
|
492
|
+
threshold: 0
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const observer = new IntersectionObserver((entries) => {
|
|
496
|
+
entries.forEach(entry => {
|
|
497
|
+
const id = entry.target.getAttribute('id');
|
|
498
|
+
const tocLink = tocList.querySelector(`[data-heading-id="${id}"]`);
|
|
499
|
+
|
|
500
|
+
if (entry.isIntersecting) {
|
|
501
|
+
// Remove active from all links
|
|
502
|
+
tocList.querySelectorAll('.toc-link').forEach(link => {
|
|
503
|
+
link.classList.remove('active');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Add active to current link
|
|
507
|
+
if (tocLink) {
|
|
508
|
+
tocLink.classList.add('active');
|
|
509
|
+
|
|
510
|
+
// Scroll TOC to show active item
|
|
511
|
+
const tocNav = document.getElementById('toc-nav');
|
|
512
|
+
if (tocNav) {
|
|
513
|
+
const linkRect = tocLink.getBoundingClientRect();
|
|
514
|
+
const navRect = tocNav.getBoundingClientRect();
|
|
515
|
+
|
|
516
|
+
if (linkRect.top < navRect.top || linkRect.bottom > navRect.bottom) {
|
|
517
|
+
tocLink.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
}, observerOptions);
|
|
524
|
+
|
|
525
|
+
// Observe all headings
|
|
526
|
+
headings.forEach(heading => {
|
|
527
|
+
observer.observe(heading);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Helper function to create URL-friendly slugs
|
|
532
|
+
function slugify(text) {
|
|
533
|
+
return text
|
|
534
|
+
.toString()
|
|
535
|
+
.toLowerCase()
|
|
536
|
+
.trim()
|
|
537
|
+
.replace(/\s+/g, '-') // Replace spaces with -
|
|
538
|
+
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
|
|
539
|
+
.replace(/\-\-+/g, '-') // Replace multiple - with single -
|
|
540
|
+
.replace(/^-+/, '') // Trim - from start
|
|
541
|
+
.replace(/-+$/, ''); // Trim - from end
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// =======================
|
|
545
|
+
// External Links Handler
|
|
546
|
+
// =======================
|
|
547
|
+
|
|
548
|
+
function initExternalLinks() {
|
|
549
|
+
// Get all links in the post content
|
|
550
|
+
const postContent = document.querySelector('.post-content');
|
|
551
|
+
if (!postContent) return;
|
|
552
|
+
|
|
553
|
+
const links = postContent.querySelectorAll('a[href]');
|
|
554
|
+
|
|
555
|
+
links.forEach(link => {
|
|
556
|
+
const href = link.getAttribute('href');
|
|
557
|
+
|
|
558
|
+
// Check if link is external (starts with http:// or https://)
|
|
559
|
+
// and doesn't link to the current domain
|
|
560
|
+
if (href && (href.startsWith('http://') || href.startsWith('https://'))) {
|
|
561
|
+
const currentDomain = window.location.hostname;
|
|
562
|
+
const linkDomain = new URL(href).hostname;
|
|
563
|
+
|
|
564
|
+
if (linkDomain !== currentDomain) {
|
|
565
|
+
// Add target="_blank" and security attributes
|
|
566
|
+
link.setAttribute('target', '_blank');
|
|
567
|
+
link.setAttribute('rel', 'noopener noreferrer');
|
|
568
|
+
|
|
569
|
+
// Add external link icon if not already present
|
|
570
|
+
if (!link.querySelector('.external-link-icon')) {
|
|
571
|
+
const icon = document.createElement('span');
|
|
572
|
+
icon.className = 'external-link-icon';
|
|
573
|
+
icon.setAttribute('aria-label', '(si apre in una nuova scheda)');
|
|
574
|
+
icon.innerHTML = '↗';
|
|
575
|
+
link.appendChild(icon);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// =======================
|
|
583
|
+
// Page Load Animations
|
|
584
|
+
// =======================
|
|
585
|
+
|
|
586
|
+
function initPageAnimations() {
|
|
587
|
+
// Animate hero section if present (home page)
|
|
588
|
+
const hero = document.querySelector('.hero');
|
|
589
|
+
if (hero) {
|
|
590
|
+
hero.classList.add('hero-animate');
|
|
591
|
+
// Animate hero children
|
|
592
|
+
const heroChildren = hero.querySelectorAll('.hero-title, .hero-subtitle, .hero-tagline, .hero-description, .hero-cta');
|
|
593
|
+
heroChildren.forEach(child => child.classList.add('animate-on-load'));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Animate post hero section if present
|
|
597
|
+
const postHero = document.querySelector('.post-hero');
|
|
598
|
+
if (postHero) {
|
|
599
|
+
postHero.classList.add('post-hero-animate');
|
|
600
|
+
// Animate post hero children
|
|
601
|
+
const postHeroChildren = postHero.querySelectorAll('.post-hero-overlay, .post-hero-date, .post-hero-categories, .post-hero-title, .post-hero-subtitle, .post-hero-reading-time');
|
|
602
|
+
postHeroChildren.forEach(child => child.classList.add('animate-on-load'));
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Animate post header if present (posts without hero image)
|
|
606
|
+
const postHeader = document.querySelector('.post-header');
|
|
607
|
+
if (postHeader) {
|
|
608
|
+
postHeader.classList.add('post-header-animate');
|
|
609
|
+
// Animate post header children
|
|
610
|
+
const postHeaderChildren = postHeader.querySelectorAll('.post-date, .post-categories, .post-title, .post-subtitle, .post-meta');
|
|
611
|
+
postHeaderChildren.forEach(child => child.classList.add('animate-on-load'));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Animate navigation
|
|
615
|
+
const nav = document.querySelector('.nav');
|
|
616
|
+
if (nav) {
|
|
617
|
+
nav.classList.add('nav-animate', 'animate-on-load');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Animate main content sections
|
|
621
|
+
const mainContent = document.querySelector('#main-content, main');
|
|
622
|
+
if (mainContent) {
|
|
623
|
+
mainContent.classList.add('content-animate', 'animate-on-load');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Animate blog post cards with stagger effect
|
|
627
|
+
const blogCards = document.querySelectorAll('.blog-post-card, .post-card');
|
|
628
|
+
blogCards.forEach((card, index) => {
|
|
629
|
+
card.classList.add('card-animate', 'animate-on-load');
|
|
630
|
+
// Add staggered delay for each card (max 8 cards with visible delay)
|
|
631
|
+
if (index < 8) {
|
|
632
|
+
card.classList.add(`delay-${index + 1}`);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// Animate footer
|
|
637
|
+
const footer = document.querySelector('.footer');
|
|
638
|
+
if (footer) {
|
|
639
|
+
// Use Intersection Observer for footer animation when it comes into view
|
|
640
|
+
if ('IntersectionObserver' in window) {
|
|
641
|
+
const footerObserver = new IntersectionObserver((entries) => {
|
|
642
|
+
entries.forEach(entry => {
|
|
643
|
+
if (entry.isIntersecting) {
|
|
644
|
+
entry.target.classList.add('fade-in-up', 'animate-on-load');
|
|
645
|
+
footerObserver.unobserve(entry.target);
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}, {
|
|
649
|
+
threshold: 0.1,
|
|
650
|
+
rootMargin: '0px 0px -50px 0px'
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
footerObserver.observe(footer);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Animate sections on scroll using Intersection Observer
|
|
658
|
+
const animateSections = document.querySelectorAll('.blog-section, .contact-section, .category-section, section');
|
|
659
|
+
|
|
660
|
+
if ('IntersectionObserver' in window && animateSections.length > 0) {
|
|
661
|
+
const sectionObserver = new IntersectionObserver((entries) => {
|
|
662
|
+
entries.forEach(entry => {
|
|
663
|
+
if (entry.isIntersecting) {
|
|
664
|
+
entry.target.classList.add('fade-in-up');
|
|
665
|
+
sectionObserver.unobserve(entry.target);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
}, {
|
|
669
|
+
threshold: 0.15,
|
|
670
|
+
rootMargin: '0px 0px -100px 0px'
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
animateSections.forEach(section => {
|
|
674
|
+
// Skip sections that already have animations
|
|
675
|
+
if (!section.classList.contains('hero') && !section.querySelector('.hero')) {
|
|
676
|
+
sectionObserver.observe(section);
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Animate post content elements on scroll
|
|
682
|
+
const postContent = document.querySelector('.post-content');
|
|
683
|
+
if (postContent && 'IntersectionObserver' in window) {
|
|
684
|
+
const contentElements = postContent.querySelectorAll('h2, h3, p, ul, ol, blockquote, pre, img, .highlight');
|
|
685
|
+
|
|
686
|
+
const contentObserver = new IntersectionObserver((entries) => {
|
|
687
|
+
entries.forEach(entry => {
|
|
688
|
+
if (entry.isIntersecting) {
|
|
689
|
+
entry.target.classList.add('fade-in-up');
|
|
690
|
+
contentObserver.unobserve(entry.target);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
}, {
|
|
694
|
+
threshold: 0.1,
|
|
695
|
+
rootMargin: '0px 0px -50px 0px'
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
contentElements.forEach((element, index) => {
|
|
699
|
+
// Add small staggered delay for consecutive elements
|
|
700
|
+
element.style.opacity = '0';
|
|
701
|
+
element.style.animationDelay = `${Math.min(index * 0.05, 0.3)}s`;
|
|
702
|
+
contentObserver.observe(element);
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|