create facts section & by the way reengineer the IntersectionObserver function

This commit is contained in:
Michi 2025-10-15 21:32:26 +02:00
parent 7e6957ce7e
commit a7df408049
11 changed files with 175 additions and 18 deletions

View file

@ -78,6 +78,10 @@ content:
- name: websiteId
label: Umami Website ID
type: string
- name: factsUrl
label: Facts JSON URL
description: URL to fetch facts data from
type: string
- name: socialmedia
label: Social Media

6
content/de/facts.md Normal file
View file

@ -0,0 +1,6 @@
---
title: "Fakten"
slug: "facts"
---
Einige Fakten über mich...

6
content/en/facts.md Normal file
View file

@ -0,0 +1,6 @@
---
title: "Facts"
slug: "facts"
---
Some facts about me...

View file

@ -99,3 +99,4 @@ defaultContentLanguage = 'de'
twitterUsername = "@michivonah"
umamiUrl = "https://data.mchvnh.ch/script.js"
websiteId = "9b188ed8-77b0-4238-aef5-c1b3d48106e4"
factsUrl = "https://cdn.michivonah.ch/facts.json"

View file

@ -16,3 +16,4 @@ no-more-projects = 'Keine weiteren Projekte verfügbar'
faq = 'Häufige Fragen'
faq-short = 'FAQ'
close = 'Schliessen'
facts = 'Fakten'

View file

@ -16,3 +16,4 @@ no-more-projects = 'No more projects available'
faq = 'Frequently Asked Questions'
faq-short = 'FAQ'
close = 'Close'
facts = 'Facts'

View file

@ -13,6 +13,8 @@
{{ partial "projects.html" . }}
{{ partial "facts.html" . }}
{{ partial "faq.html" . }}
</div>

View file

@ -0,0 +1,34 @@
<div id="facts" class="facts">
<div class="facts-intro fade-up">
{{ with .Site.GetPage "facts" }}
<h1>{{ .Title }}</h1>
<p>{{ .Content }}</p>
{{ end }}
</div>
<div class="fact-container">
{{ $url := .Site.Params.factsUrl }}
{{ with try (resources.GetRemote $url) }}
{{ with .Err }}
{{ errorf "%s" . }}
{{ else with .Value }}
{{ $facts := . | transform.Unmarshal }}
{{ if $facts }}
{{ range $fact := $facts }}
{{ $count := 0 }}
{{ if isset $fact "total" }}
{{ $count = $fact.total }}
{{ else if isset $fact "items" }}
{{ $count = len $fact.items }}
{{ end }}
<div class="fact fade-up">
<p class="fact-counter" data-count="{{ $count }}"></p>
<p>{{ index $fact.titles $.Lang }}</p>
</div>
{{ end }}
{{ else }}
{{ errorf "Unable to get remote resource %q" $url }}
{{ end }}
{{ end }}
{{ end }}
</div>
</div>

View file

@ -5,6 +5,7 @@
<a href="#" alt='{{ T "home" }}'>{{ T "home" }}</a>
<a href="#about" alt='{{ T "about" }}'>{{ T "about" }}</a>
<a href="#projects" alt='{{ T "projects" }}'>{{ T "projects" }}</a>
<a href="#facts" alt='{{ T "facts" }}'>{{ T "facts" }}</a>
<a href="#faq" alt='{{ T "faq-short" }}'>{{ T "faq-short" }}</a>
<a href="https://blog.michivonah.ch" alt='{{ T "blog" }}'>{{ T "blog" }}</a>
<a href="#contact" alt='{{ T "contact" }}'>{{ T "contact" }}</a>

View file

@ -100,29 +100,65 @@ function updateNavStyle(){
document.querySelector("nav").classList.toggle("small", window.scrollY > 20);
}
// intersection observer for animations
// credits: https://coolcssanimation.com/how-to-trigger-a-css-animation-on-scroll/
/**
* Template for easily creating a IntersectionObserver
* @param {*} triggerElement Element which triggers the IntersectionObserver
* @param {*} callback Function which gets interesction object back
* @param {*} rootMargin Parameter rootMargin of the IntersectionObserver
* @param {*} threshold Parameter threshold of the IntersectionObserver
* @returns IntersectionObserver
*/
function createIntersectionObserver(triggerElement, callback, rootMargin = '0px 0px 5% 0px', threshold = 0.1){
if(triggerElement){
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
callback(entry);
});
}, { rootMargin, threshold });
return observer.observe(triggerElement);
}
}
/**
* Function which applies an IntersectionObserver
* for running animations on scroll/visibility of a container.
*
* Original inspiration: https://coolcssanimation.com/how-to-trigger-a-css-animation-on-scroll/
* @param {*} triggerSelector Query selector of the element, which should trigger the animation
* @param {*} animationClass Class to append when animation is triggered
* @param {*} targetElement Element to apply the animation (css class) to. Will applied to triggerSelector if not defined.
* @returns IntersectionObserver
*/
function animationOnScroll(triggerSelector, animationClass, targetElement){
const trigger = document.querySelector(triggerSelector);
const target = ((targetElement) ? document.querySelector(targetElement) : trigger);
if(trigger){
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
target.classList.add(animationClass);
}
else{
target.classList.remove(animationClass);
}
});
}, { rootMargin: '0px 0px 5% 0px', threshold: 0.1 });
return observer.observe(trigger);
}
return createIntersectionObserver(trigger, (entry) => {
entry.isIntersecting ? target.classList.add(animationClass) : target.classList.remove(animationClass);
});
}
/**
* Creates IntersectionObserver for triggering counter animation
* @param {*} elementSelector Selector of the element(s) to apply the counter effect to
* @param {*} cssPropertyName CSS Property with counter's value
* @param {*} valueAttribute HTML data attribute with the counter's final value
*/
function countOnScroll(elementSelector, cssPropertyName, valueAttribute){
const items = document.querySelectorAll(elementSelector);
items.forEach(element => {
const value = parseInt(element.getAttribute(valueAttribute));
createIntersectionObserver(element, (entry) => {
element.style.setProperty(cssPropertyName, entry.isIntersecting ? value : 0);
});
});
}
// apply on scroll effects
animationOnScroll('.contact-title-wrapper', 'typewriter-animation', '.contact-title');
countOnScroll('.fact-counter', '--factCounter', 'data-count');
// calculate age
function calculateAge(selector){

View file

@ -777,6 +777,71 @@ nav.small .nav-links a:last-child:focus{
}
}
/* FACTS */
.facts{
margin-top: 30px;
padding-bottom: 20px;
text-align: center;
scroll-margin-top: calc(var(--navSmallHeight) + 10px);
}
.fact-container{
display: flex;
flex-direction: row;
justify-content: space-around;
}
.fact{
display: flex;
flex-direction: column;
font-size: 1.2rem;
font-weight: 600;
width: 100%;
padding-block: 40px 20px;
}
.fact p{
margin: 5px 0;
padding: 0;
}
@media screen and (max-width:690px){
.fact-container{
flex-direction: column;
}
.fact{
padding-block: 10px;
}
}
@property --factCounter{
syntax: '<integer>';
initial-value: 0;
inherits: false;
}
.fact-counter{
font-size: 3.8rem;
font-weight: 700;
color: var(--primary);
transition: --factCounter 1s, var(--baseTransition);
counter-reset: factCounter var(--factCounter);
}
@media (pointer: fine){
.fact-counter:hover,
.fact-counter:focus{
transform: scale(1.1);
transition: var(--baseTransition);
filter: drop-shadow(0 0 0.6rem var(--primary));
}
}
.fact-counter::after{
content: counter(factCounter);
}
/* FAQ */
.faq{
padding-block: 40px;