löten leuchten
|
@ -2,6 +2,7 @@
|
||||||
title = "How to miet Ulli"
|
title = "How to miet Ulli"
|
||||||
date = 2025-05-01
|
date = 2025-05-01
|
||||||
authors = ["Aron Petau"]
|
authors = ["Aron Petau"]
|
||||||
|
draft=true
|
||||||
|
|
||||||
[extra]
|
[extra]
|
||||||
show_copyright = true
|
show_copyright = true
|
||||||
|
@ -9,8 +10,9 @@ show_shares = true
|
||||||
featured = true
|
featured = true
|
||||||
+++
|
+++
|
||||||
|
|
||||||
|
{% alert(note=true) %}
|
||||||
This is a work in Progress. Informations on here are subject to change.
|
This is a work in Progress. Informations on here are subject to change.
|
||||||
{: .notice--danger}
|
{% end %}
|
||||||
|
|
||||||
<iframe width="100%" height="800" src="https://vrm.victronenergy.com/installation/167009/embed/f61b11f2"></iframe>
|
<iframe width="100%" height="800" src="https://vrm.victronenergy.com/installation/167009/embed/f61b11f2"></iframe>
|
||||||
|
|
||||||
|
@ -21,8 +23,9 @@ That means roughly 80 PS. No mountains and offroading for you in there.
|
||||||
|
|
||||||
## Propellant
|
## Propellant
|
||||||
|
|
||||||
|
{% alert(note=true) %}
|
||||||
The car takes either **Super** (95), **Super Plus** (98) or **E10**.
|
The car takes either **Super** (95), **Super Plus** (98) or **E10**.
|
||||||
{: .notice--danger}
|
{% end %}
|
||||||
|
|
||||||
The Car uses about 10-12l/100km, when never going beyond 90 km/h.
|
The Car uses about 10-12l/100km, when never going beyond 90 km/h.
|
||||||
Otherwise it uses about 12-15l/100km.
|
Otherwise it uses about 12-15l/100km.
|
||||||
|
@ -31,8 +34,9 @@ The tank is 85l, so you can go about 750km on one tank.
|
||||||
The tank needle is broken, so you have to use the trip meter to know how much fuel is left.
|
The tank needle is broken, so you have to use the trip meter to know how much fuel is left.
|
||||||
Always refill completely, otherwise you will have no clue about your range.
|
Always refill completely, otherwise you will have no clue about your range.
|
||||||
|
|
||||||
|
{% alert(note=true) %}
|
||||||
Please remember to reset the meter after filling up.
|
Please remember to reset the meter after filling up.
|
||||||
{: .notice--danger}
|
{% end %}
|
||||||
|
|
||||||
The car is a manual, so you need to know how to drive stick.
|
The car is a manual, so you need to know how to drive stick.
|
||||||
There is no Servolenkung (power steering), so it is a bit harder to steer.
|
There is no Servolenkung (power steering), so it is a bit harder to steer.
|
||||||
|
@ -52,33 +56,38 @@ The solar system functions automatically and has an automatic shut-off, so you c
|
||||||
For the Analogue people, there is a battery monitor in the car, that shows the current battery voltage. It is right next to the driver seat and can read the front starter battery and the support back battery.
|
For the Analogue people, there is a battery monitor in the car, that shows the current battery voltage. It is right next to the driver seat and can read the front starter battery and the support back battery.
|
||||||
Anything below 11.5V is considered empty and you should start the car to recharge the battery. There is a battery booster, which will charge both batteries while driving.
|
Anything below 11.5V is considered empty and you should start the car to recharge the battery. There is a battery booster, which will charge both batteries while driving.
|
||||||
|
|
||||||
|
{% alert(note=true) %}
|
||||||
For increased solar capacity, take out the folded panels, plug their cable into the adapter at the rear under the exhaust pipe and place the panels in the sun. The solar system will automatically use the additional power.
|
For increased solar capacity, take out the folded panels, plug their cable into the adapter at the rear under the exhaust pipe and place the panels in the sun. The solar system will automatically use the additional power.
|
||||||
{: .notice--info}
|
{% end %}
|
||||||
|
|
||||||
## Kitchen
|
## Kitchen
|
||||||
|
|
||||||
The kitchen is equipped with a 2 flame gas stove and a sink with running water.
|
The kitchen is equipped with a 2 flame gas stove and a sink with running water.
|
||||||
The water is stored in a 15l tank, which is filled from the outside. Using it awarely, it can last for about 3 days (2 people).
|
The water is stored in a 15l tank, which is filled from the outside. Using it awarely, it can last for about 3 days (2 people).
|
||||||
|
|
||||||
|
{% alert(note=true) %}
|
||||||
Gas is standard 5kg grey gas bottles, which can be exchanged at any gas station. There is a bottle in the car, which is usually enough for more than a month of cooking. If you run out, you can exchange it at any gas station.
|
Gas is standard 5kg grey gas bottles, which can be exchanged at any gas station. There is a bottle in the car, which is usually enough for more than a month of cooking. If you run out, you can exchange it at any gas station.
|
||||||
{: .notice--info}
|
{% end %}
|
||||||
|
|
||||||
Kitchen is fully equipped for 2 People.
|
Kitchen is fully equipped for 2 People.
|
||||||
|
|
||||||
|
{% alert(note=true) %}
|
||||||
Always roll the top window down while driving. You will loose the window and its roughly 300 to replace.
|
Always roll the top window down while driving. You will loose the window and its roughly 300 to replace.
|
||||||
{: .notice--danger}
|
{% end %}
|
||||||
|
|
||||||
There is a powerful 12v fridge which holds about 30l. It is powered by the solar system and can be used while driving. With enough sun it can run 24/7.
|
There is a powerful 12v fridge which holds about 30l. It is powered by the solar system and can be used while driving. With enough sun it can run 24/7.
|
||||||
|
|
||||||
|
{% alert(note=true) %}
|
||||||
Anything above the setting 3 will freeze your food. Level 2 works for me.
|
Anything above the setting 3 will freeze your food. Level 2 works for me.
|
||||||
{: .notice--danger}
|
{% end %}
|
||||||
|
|
||||||
## Sleeping
|
## Sleeping
|
||||||
|
|
||||||
The bed is in the high roof and is 1.20m wide and 1.90m long.
|
The bed is in the high roof and is 1.20m wide and 1.90m long.
|
||||||
|
|
||||||
|
{% alert(note=true) %}
|
||||||
There is climbing involved to get up there.
|
There is climbing involved to get up there.
|
||||||
{: .notice--info}
|
{% end %}
|
||||||
|
|
||||||
## Heating
|
## Heating
|
||||||
|
|
||||||
|
@ -105,8 +114,10 @@ Then we can arrange a time for you to pick up the car.
|
||||||
The car costs 30€ per day, plus 0.10€ per km.
|
The car costs 30€ per day, plus 0.10€ per km.
|
||||||
This factors in my insurance and the taxes I have to pay.
|
This factors in my insurance and the taxes I have to pay.
|
||||||
|
|
||||||
Any damages to the car will be charged to you. A total damage would cost you somewhere around 10.000 Euro, so please be careful.
|
{% alert(note=true) %}
|
||||||
|
Any damages to the car will be charged to you.
|
||||||
|
A total damage would cost you somewhere around 10.000 Euro, so please be careful.
|
||||||
Check your Haftpflichtversicherung (private liability insurance) to see whether it covers rented cars.
|
Check your Haftpflichtversicherung (private liability insurance) to see whether it covers rented cars.
|
||||||
{: .notice--danger}
|
{% end %}
|
||||||
|
|
||||||
[Benzinrechner](https://benzinrechner.info/en)
|
[Benzinrechner](https://benzinrechner.info/en)
|
||||||
|
|
After Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 181 KiB |
BIN
content/project/2025-05-16-einszwovier-löten-leuchten/hand.jpeg
Normal file
After Width: | Height: | Size: 72 KiB |
|
@ -0,0 +1,102 @@
|
||||||
|
+++
|
||||||
|
title = "einszwovier: löten und leuchten"
|
||||||
|
date = 2025-05-16
|
||||||
|
authors = ["Aron Petau", "Friedrich Weber"]
|
||||||
|
description = "The first course format in einszwovier: löten und leuchten"
|
||||||
|
draft = false
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = [
|
||||||
|
"making",
|
||||||
|
"education",
|
||||||
|
"democratic",
|
||||||
|
"engineering",
|
||||||
|
"experiment",
|
||||||
|
"work",
|
||||||
|
"3D printing",
|
||||||
|
"soldering",
|
||||||
|
"electronics",
|
||||||
|
"einszwovier",
|
||||||
|
]
|
||||||
|
[extra]
|
||||||
|
banner = "eins zwo vier logo.png"
|
||||||
|
show_copyright = true
|
||||||
|
show_shares = true
|
||||||
|
featured = false
|
||||||
|
+++
|
||||||
|
|
||||||
|
|
||||||
|
{% gallery() %}
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"file": "cables.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": "All the led Lamps together"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "feedback.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": "The Guestbook: a quick Feedback mechanism we use"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "hand.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": "Tinkereing with only simple shapes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "lights.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": "More Lights"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "prints.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": "Some overmight prints"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "ski.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": "A completely self-designed skier"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
**Ein praxisnaher Kurs zu Löten, Elektronik und Lampendesign für junge Tüftler*innen**
|
||||||
|
|
||||||
|
*Löten und Leuchten* fand inzwischen in drei erfolgreichen Durchläufen statt — jeweils als Angebot für Schüler*innen der 5. und 6. Klasse. Der Kurs bietet einen spielerischen und begleiteten Einstieg in die Welt der Elektronik, des Lötens und der digitalen Gestaltung. Im Mittelpunkt steht das **Verstehen durch eigenes Machen**: Technologien begreifen, indem man sie selbst gestaltet.
|
||||||
|
|
||||||
|
## Das Projekt
|
||||||
|
|
||||||
|
Über drei Sitzungen hinweg (jeweils drei Stunden) entwickelten und bauten die Kinder ihre eigene USB-betriebene LED-Leuchte. Sie löteten elektronische Bauteile, modellierten Gehäuse in 3D, beschäftigten sich mit Lichtstreuung und lernten dabei ganz selbstverständlich, technische Probleme kreativ zu lösen. Jede Leuchte wurde von Grund auf gebaut, funktional und transportabel – ganz ohne Batterien, dafür mit echten Kabeln, Werkzeug und einem großen Schuss Eigenverantwortung.
|
||||||
|
|
||||||
|
Zum Einstieg lernten die Teilnehmer*innen die Grundlagen der Elektrizität mit den wunderbar zugänglichen **Makey Makey**-Boards kennen. Damit konnten wir spielerisch Stromkreise, Leitfähigkeit und Steuerung erklären – ein Einstieg, der sofort Neugier und Begeisterung weckte.
|
||||||
|
|
||||||
|
Anschließend folgte das Herzstück des Projekts: **USB-Kabel aufschneiden, 5V-LEDs anlöten** und eigene Gehäuse entwerfen. Das Löten geschah unter Aufsicht, aber jede*r lötete selbst – und das mit sichtbarem Stolz. Wenn die eigene LED zum ersten Mal leuchtet, ist das ein magischer Moment.
|
||||||
|
|
||||||
|
## Gestaltung mit Werkzeug – und mit Einschränkungen
|
||||||
|
|
||||||
|
Für die 3D-Gestaltung nutzten wir **Tinkercad** auf iPads. Die Oberfläche war für viele der erste Berührungspunkt mit CAD-Software und erwies sich als zugänglich und intuitiv – allerdings nicht ohne technische Stolpersteine. Tinkercad stürzte gelegentlich ab, und Synchronisationsprobleme führten manchmal zu Verwirrung. Trotz dieser Hürden ermöglichte es einen **niedrigschwelligen Einstieg in die digitale Gestaltung**.
|
||||||
|
|
||||||
|
Die entworfenen Lampenschirme mussten nicht nur schön aussehen, sondern auch die Elektronik sinnvoll aufnehmen. Dadurch ergaben sich ganz reale Designherausforderungen: Passt das Kabel? Wie weit darf die LED vom Gehäuse entfernt sein? Wie verändert sich das Licht?
|
||||||
|
|
||||||
|
Gedruckt wurde mit weißem PLA-Filament – ideal für die Lichtstreuung. Im Kurs entwickelten sich dadurch ganz organisch Gespräche über **Materialeigenschaften, Lichtdurchlässigkeit und die physikalischen Grenzen des 3D-Drucks**.
|
||||||
|
|
||||||
|
## Echte Herausforderungen, echtes Denken
|
||||||
|
|
||||||
|
Das Projekt traf genau die richtige Balance: **anspruchsvoll genug, um ernst genommen zu werden**, aber machbar genug, damit alle ein Erfolgserlebnis hatten. Jedes Kind nahm am Ende eine funktionierende, selbstgebaute Lampe mit nach Hause – und keine glich der anderen.
|
||||||
|
|
||||||
|
Dabei gab es viele kleine Hürden: USB-Kabel, die zu viel Spiel hatten, Gehäuse, die nicht sofort passten, LEDs, die nachjustiert werden mussten. Wir wichen diesen Herausforderungen nicht aus – im Gegenteil: Wir nutzten sie als Anlässe, um gemeinsam nach Lösungen zu suchen. Gerade diese Momente führten zu den besten Gesprächen über Technik, Entwurf und Fehlerkultur.
|
||||||
|
|
||||||
|
## Bonus-Runde: Tischkicker-Prototypen
|
||||||
|
|
||||||
|
Zum Abschluss durfte jede Gruppe ihren eigenen **Mini-Tischkicker** entwerfen – mit den Materialien und Ideen, die sie zur Verfügung hatten. Diese kreative Extra-Aufgabe förderte **Teamarbeit, Improvisation und erste Design-Thinking-Schritte**. Und ganz nebenbei entstanden viele lustige, kluge und überraschende Lösungen.
|
||||||
|
|
||||||
|
## Rückblick
|
||||||
|
|
||||||
|
Alle drei Durchgänge des Workshops wurden mit **großem Interesse, Konzentration und Freude** aufgenommen. Die Kinder waren über die gesamte Zeit engagiert, nicht nur beim Basteln, sondern auch im Denken: Wie funktioniert das? Was kann ich anders machen? Was ist möglich?
|
||||||
|
|
||||||
|
Sie gingen nicht nur mit einer leuchtenden Lampe nach Hause – sondern mit dem Gefühl, etwas **selbst geschaffen** zu haben. Und mit der Erkenntnis, dass Technik keine Zauberei ist, sondern etwas, das man verstehen und gestalten kann.
|
||||||
|
|
||||||
|
Auch für uns als Kursleitung war *Löten und Leuchten* ein bestärkendes Erlebnis. Die Kombination aus digitalen Werkzeugen, praktischer Arbeit und offener Aufgabenstellung schuf einen Raum, in dem Lernen ganz selbstverständlich und mit echter Neugier geschah.
|
||||||
|
|
||||||
|
*Löten und Leuchten* wird sich weiterentwickeln – doch das Ziel bleibt dasselbe: **Kinder stärken, selbstbestimmt mit Technik umzugehen, und ihnen zeigen, dass sie mehr können, als sie denken.**
|
101
content/project/2025-05-16-einszwovier-löten-leuchten/index.md
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
+++
|
||||||
|
title = "einszwovier: löten und leuchten"
|
||||||
|
date = 2025-05-16
|
||||||
|
authors = ["Aron Petau", "Friedrich Weber"]
|
||||||
|
description = "The first course format in einszwovier: löten und leuchten"
|
||||||
|
draft = false
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = [
|
||||||
|
"making",
|
||||||
|
"education",
|
||||||
|
"democratic",
|
||||||
|
"engineering",
|
||||||
|
"experiment",
|
||||||
|
"work",
|
||||||
|
"3D printing",
|
||||||
|
"soldering",
|
||||||
|
"electronics",
|
||||||
|
"einszwovier",
|
||||||
|
]
|
||||||
|
[extra]
|
||||||
|
banner = "eins zwo vier logo.png"
|
||||||
|
show_copyright = true
|
||||||
|
show_shares = true
|
||||||
|
featured = false
|
||||||
|
+++
|
||||||
|
|
||||||
|
|
||||||
|
{% gallery() %}
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"file": "cables.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": "All the led Lamps together"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "feedback.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": "The Guestbook: a quick Feedback mechanism we use"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "hand.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": "Tinkereing with only simple shapes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "lights.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": "More Lights"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "prints.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": "Some overmight prints"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "ski.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": "A completely self-designed skier"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
|
||||||
|
# Löten und Leuchten
|
||||||
|
|
||||||
|
**A hands-on course in soldering, electronics, and lamp design for young creators**
|
||||||
|
|
||||||
|
*Löten und Leuchten* has now run in three successful iterations — each time offering 5th and 6th graders a guided yet exploratory dive into the worlds of electronics, making, and digital design. At its core, the course is about **understanding through creating**: introducing young learners to tangible technologies and encouraging them to shape the outcome with their own ideas and hands.
|
||||||
|
|
||||||
|
## The Project
|
||||||
|
|
||||||
|
Over three sessions (each lasting three hours), participants designed and built their own USB-powered LED lamp. Along the way, they soldered electronic components, modeled lamp housings in 3D, learned about light diffusion, and got a direct introduction to real-world problem solving. Every lamp was built from scratch, powered via USB — no batteries, no glue kits, just wire, plastic, and a bit of courage.
|
||||||
|
|
||||||
|
The children began by learning the basics of electricity through interactive experiments using the excellent **Makey Makey** boards. These allowed us to demonstrate concepts like conductivity, input/output, and circuitry in a playful and intuitive way. The enthusiasm was immediate and contagious.
|
||||||
|
|
||||||
|
From there, we moved to the heart of the project: **cutting open USB cables, preparing and soldering 5V LEDs**, and designing enclosures for them. The soldering was always supervised, but each child did their own work — and it showed. There's something deeply satisfying about holding a working circuit you assembled yourself, and many kids expressed how proud they were to see their light turn on.
|
||||||
|
|
||||||
|
## Designing with Tools — and Constraints
|
||||||
|
|
||||||
|
For 3D modeling, we used **Tinkercad** on iPads. While the interface proved very accessible, we also encountered its limits: the app occasionally crashed or froze under load, and file syncing sometimes led to confusion. Nonetheless, it provided a **gentle, well-mediated entry point** to CAD. Most kids had never touched 3D design software before, but quickly began exploring shapes, tolerances, and fitting dimensions. The lamps they created weren’t just decorative — they had to **functionally hold the electronics**, which added a very real-world layer of complexity.
|
||||||
|
|
||||||
|
The printed shades were all done in white PLA to support light diffusion. This led to organic conversations around **material properties, translucency, and light behavior**, which the kids quickly absorbed and applied in their designs.
|
||||||
|
|
||||||
|
## Real Challenges, Real Thinking
|
||||||
|
|
||||||
|
The project hit a sweet spot: **it was challenging enough to be meaningful**, but achievable enough to allow for success. Every child managed to finish a working lamp — and each one was different. Along the way, they encountered plenty of design hurdles: USB cables that needed reinforcement, cases that didn’t fit on the first try, LEDs that had to be repositioned for optimal glow.
|
||||||
|
|
||||||
|
We didn’t avoid these issues — we embraced them. Instead of simplifying the process to a formula, we treated every obstacle as an opportunity for discussion. Why didn’t this fit? What could we change? How do you fix it? These moments turned into some of the richest learning experiences in the course.
|
||||||
|
|
||||||
|
## Bonus Round: Tabletop Foosball
|
||||||
|
|
||||||
|
As a closing challenge, each group designed their own **mini foosball table**, using whatever materials and approaches they liked. This final task was light-hearted, but not without its own design challenges — and it served as a great entry into **collaborative thinking and prototyping**. It also reinforced our goal of learning through play, iteration, and autonomy.
|
||||||
|
|
||||||
|
## Reflections
|
||||||
|
|
||||||
|
Across all three runs, the workshop was met with **enthusiasm, curiosity, and real focus**. The kids were engaged from start to finish, not just with the tools, but with the ideas behind them. They walked away with more than just a glowing lamp — they gained an understanding of how things work, and a confidence that they can build things themselves.
|
||||||
|
|
||||||
|
For us as facilitators, the course reaffirmed how powerful hands-on, self-directed learning can be. The combination of digital and physical tools, real constraints, and open-ended outcomes created an environment where creativity thrived.
|
||||||
|
|
||||||
|
*Löten und Leuchten* will continue to evolve, but its core will remain: empowering kids to build things they care about, and helping them realize that technology isn’t magic — it’s something they can shape.
|
After Width: | Height: | Size: 105 KiB |
After Width: | Height: | Size: 226 KiB |
BIN
content/project/2025-05-16-einszwovier-löten-leuchten/ski.jpeg
Normal file
After Width: | Height: | Size: 79 KiB |
|
@ -14,6 +14,7 @@ tags = [
|
||||||
"experiment",
|
"experiment",
|
||||||
"work",
|
"work",
|
||||||
"3D printing",
|
"3D printing",
|
||||||
|
"einszwovier",
|
||||||
]
|
]
|
||||||
[extra]
|
[extra]
|
||||||
banner = "eins zwo vier logo.png"
|
banner = "eins zwo vier logo.png"
|
||||||
|
|
BIN
public/404.gif
Normal file
After Width: | Height: | Size: 4 KiB |
2
public/404.html
Normal file
BIN
public/404.png
Normal file
After Width: | Height: | Size: 340 B |
BIN
public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 28 KiB |
3931
public/atom.xml
Normal file
1
public/auto-render.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,(function(e){return function(){"use strict";var t={771:function(t){t.exports=e}},n={};function r(e){var o=n[e];if(void 0!==o)return o.exports;var i=n[e]={exports:{}};return t[e](i,i.exports,r),i.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var o={};return function(){r.d(o,{default:function(){return d}});var e=r(771),t=r.n(e);const n=function(e,t,n){let r=n,o=0;const i=e.length;for(;r<t.length;){const n=t[r];if(o<=0&&t.slice(r,r+i)===e)return r;"\\"===n?r++:"{"===n?o++:"}"===n&&o--,r++}return-1},i=/^\\begin{/;var a=function(e,t){let r;const o=[],a=new RegExp("("+t.map((e=>e.left.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"))).join("|")+")");for(;r=e.search(a),-1!==r;){r>0&&(o.push({type:"text",data:e.slice(0,r)}),e=e.slice(r));const a=t.findIndex((t=>e.startsWith(t.left)));if(r=n(t[a].right,e,t[a].left.length),-1===r)break;const l=e.slice(0,r+t[a].right.length),s=i.test(l)?l:e.slice(t[a].left.length,r);o.push({type:"math",data:s,rawData:l,display:t[a].display}),e=e.slice(r+t[a].right.length)}return""!==e&&o.push({type:"text",data:e}),o};const l=function(e,n){const r=a(e,n.delimiters);if(1===r.length&&"text"===r[0].type)return null;const o=document.createDocumentFragment();for(let e=0;e<r.length;e++)if("text"===r[e].type)o.appendChild(document.createTextNode(r[e].data));else{const i=document.createElement("span");let a=r[e].data;n.displayMode=r[e].display;try{n.preProcess&&(a=n.preProcess(a)),t().render(a,i,n)}catch(i){if(!(i instanceof t().ParseError))throw i;n.errorCallback("KaTeX auto-render: Failed to parse `"+r[e].data+"` with ",i),o.appendChild(document.createTextNode(r[e].rawData));continue}o.appendChild(i)}return o},s=function(e,t){for(let n=0;n<e.childNodes.length;n++){const r=e.childNodes[n];if(3===r.nodeType){let o=r.textContent,i=r.nextSibling,a=0;for(;i&&i.nodeType===Node.TEXT_NODE;)o+=i.textContent,i=i.nextSibling,a++;const s=l(o,t);if(s){for(let e=0;e<a;e++)r.nextSibling.remove();n+=s.childNodes.length-1,e.replaceChild(s,r)}else n+=a}else if(1===r.nodeType){const e=" "+r.className+" ";-1===t.ignoredTags.indexOf(r.nodeName.toLowerCase())&&t.ignoredClasses.every((t=>-1===e.indexOf(" "+t+" ")))&&s(r,t)}}};var d=function(e,t){if(!e)throw new Error("No element provided to render");const n={};for(const e in t)t.hasOwnProperty(e)&&(n[e]=t[e]);n.delimiters=n.delimiters||[{left:"$$",right:"$$",display:!0},{left:"\\(",right:"\\)",display:!1},{left:"\\begin{equation}",right:"\\end{equation}",display:!0},{left:"\\begin{align}",right:"\\end{align}",display:!0},{left:"\\begin{alignat}",right:"\\end{alignat}",display:!0},{left:"\\begin{gather}",right:"\\end{gather}",display:!0},{left:"\\begin{CD}",right:"\\end{CD}",display:!0},{left:"\\[",right:"\\]",display:!0}],n.ignoredTags=n.ignoredTags||["script","noscript","style","textarea","pre","code","option"],n.ignoredClasses=n.ignoredClasses||[],n.errorCallback=n.errorCallback||console.error,n.macros=n.macros||{},s(e,n)}}(),o=o.default}()}));
|
BIN
public/card.png
Normal file
After Width: | Height: | Size: 1.2 MiB |
27
public/closable.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
const closable = document.querySelectorAll("details.closable");
|
||||||
|
|
||||||
|
closable.forEach((detail) => {
|
||||||
|
detail.addEventListener("toggle", () => {
|
||||||
|
if (detail.open) setTargetDetail(detail);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setTargetDetail(targetDetail) {
|
||||||
|
closable.forEach((detail) => {
|
||||||
|
if (detail !== targetDetail) {
|
||||||
|
detail.open = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("click", function (event) {
|
||||||
|
const isClickInsideDetail = [...closable].some((detail) =>
|
||||||
|
detail.contains(event.target)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isClickInsideDetail) {
|
||||||
|
closable.forEach((detail) => {
|
||||||
|
detail.open = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
406
public/comments.js
Normal file
|
@ -0,0 +1,406 @@
|
||||||
|
// Taken from https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon/
|
||||||
|
// Attachment, card, and spoiler code taken from https://github.com/cassidyjames/cassidyjames.github.io/blob/99782788a7e3ba3cc52d6803010873abd1b02b9e/_includes/comments.html#L251-L296
|
||||||
|
|
||||||
|
let blogPostAuthorText = document.getElementById("blog-post-author-text").textContent;
|
||||||
|
let boostsFromText = document.getElementById("boosts-from-text").textContent;
|
||||||
|
let dateLocale = document.getElementById("date-locale").textContent;
|
||||||
|
let favesFromText = document.getElementById("faves-from-text").textContent;
|
||||||
|
let host = document.getElementById("host").textContent;
|
||||||
|
let id = document.getElementById("id").textContent;
|
||||||
|
let lazyAsyncImage = document.getElementById("lazy-async-image").textContent;
|
||||||
|
let loadingText = document.getElementById("loading-text").textContent;
|
||||||
|
let noCommentsText = document.getElementById("no-comments-text").textContent;
|
||||||
|
let relAttributes = document.getElementById("rel-attributes").textContent;
|
||||||
|
let reloadText = document.getElementById("reload-text").textContent;
|
||||||
|
let sensitiveText = document.getElementById("sensitive-text").textContent;
|
||||||
|
let user = document.getElementById("user").textContent;
|
||||||
|
let viewCommentText = document.getElementById("view-comment-text").textContent;
|
||||||
|
let viewProfileText = document.getElementById("view-profile-text").textContent;
|
||||||
|
|
||||||
|
document.getElementById("load-comments").addEventListener("click", loadComments);
|
||||||
|
|
||||||
|
function escapeHtml(unsafe) {
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
function emojify(input, emojis) {
|
||||||
|
let output = input;
|
||||||
|
|
||||||
|
emojis.forEach((emoji) => {
|
||||||
|
let picture = document.createElement("picture");
|
||||||
|
|
||||||
|
let source = document.createElement("source");
|
||||||
|
source.setAttribute("srcset", escapeHtml(emoji.url));
|
||||||
|
source.setAttribute("media", "(prefers-reduced-motion: no-preference)");
|
||||||
|
|
||||||
|
let img = document.createElement("img");
|
||||||
|
img.className = "emoji";
|
||||||
|
img.setAttribute("src", escapeHtml(emoji.static_url));
|
||||||
|
img.setAttribute("alt", `:${emoji.shortcode}:`);
|
||||||
|
img.setAttribute("title", `:${emoji.shortcode}:`);
|
||||||
|
if (lazyAsyncImage == "true") {
|
||||||
|
img.setAttribute("decoding", "async");
|
||||||
|
img.setAttribute("loading", "lazy");
|
||||||
|
}
|
||||||
|
|
||||||
|
picture.appendChild(source);
|
||||||
|
picture.appendChild(img);
|
||||||
|
|
||||||
|
output = output.replace(`:${emoji.shortcode}:`, picture.outerHTML);
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadComments() {
|
||||||
|
let commentsWrapper = document.getElementById("comments-wrapper");
|
||||||
|
commentsWrapper.innerHTML = "";
|
||||||
|
|
||||||
|
let loadCommentsButton = document.getElementById("load-comments");
|
||||||
|
loadCommentsButton.innerHTML = loadingText;
|
||||||
|
loadCommentsButton.disabled = true;
|
||||||
|
|
||||||
|
fetch(`https://${host}/api/v1/statuses/${id}/context`)
|
||||||
|
.then(function (response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function (data) {
|
||||||
|
let descendants = data["descendants"];
|
||||||
|
if (
|
||||||
|
descendants &&
|
||||||
|
Array.isArray(descendants) &&
|
||||||
|
descendants.length > 0
|
||||||
|
) {
|
||||||
|
commentsWrapper.innerHTML = "";
|
||||||
|
|
||||||
|
descendants.forEach(function (status) {
|
||||||
|
console.log(descendants);
|
||||||
|
if (status.account.display_name.length > 0) {
|
||||||
|
status.account.display_name = escapeHtml(
|
||||||
|
status.account.display_name
|
||||||
|
);
|
||||||
|
status.account.display_name = emojify(
|
||||||
|
status.account.display_name,
|
||||||
|
status.account.emojis
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
status.account.display_name = status.account.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
let instance = "";
|
||||||
|
if (status.account.acct.includes("@")) {
|
||||||
|
instance = status.account.acct.split("@")[1];
|
||||||
|
} else {
|
||||||
|
instance = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isReply = status.in_reply_to_id !== id;
|
||||||
|
|
||||||
|
let op = false;
|
||||||
|
if (status.account.acct == user) {
|
||||||
|
op = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
status.content = emojify(status.content, status.emojis);
|
||||||
|
|
||||||
|
let comment = document.createElement("article");
|
||||||
|
comment.id = `comment-${status.id}`;
|
||||||
|
comment.className = isReply ? "comment comment-reply" : "comment";
|
||||||
|
comment.setAttribute("itemprop", "comment");
|
||||||
|
comment.setAttribute("itemtype", "http://schema.org/Comment");
|
||||||
|
|
||||||
|
let avatarSource = document.createElement("source");
|
||||||
|
avatarSource.setAttribute(
|
||||||
|
"srcset",
|
||||||
|
escapeHtml(status.account.avatar)
|
||||||
|
);
|
||||||
|
avatarSource.setAttribute(
|
||||||
|
"media",
|
||||||
|
"(prefers-reduced-motion: no-preference)"
|
||||||
|
);
|
||||||
|
|
||||||
|
let avatarImg = document.createElement("img");
|
||||||
|
avatarImg.className = "avatar";
|
||||||
|
avatarImg.setAttribute(
|
||||||
|
"src",
|
||||||
|
escapeHtml(status.account.avatar_static)
|
||||||
|
);
|
||||||
|
avatarImg.setAttribute(
|
||||||
|
"alt",
|
||||||
|
`@${status.account.username}@${instance} avatar`
|
||||||
|
);
|
||||||
|
if (lazyAsyncImage == "true") {
|
||||||
|
avatarImg.setAttribute("decoding", "async");
|
||||||
|
avatarImg.setAttribute("loading", "lazy");
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarPicture = document.createElement("picture");
|
||||||
|
avatarPicture.appendChild(avatarSource);
|
||||||
|
avatarPicture.appendChild(avatarImg);
|
||||||
|
|
||||||
|
let avatar = document.createElement("a");
|
||||||
|
avatar.className = "avatar-link";
|
||||||
|
avatar.setAttribute("href", status.account.url);
|
||||||
|
avatar.setAttribute("rel", relAttributes);
|
||||||
|
avatar.setAttribute(
|
||||||
|
"title",
|
||||||
|
`${viewProfileText} @${status.account.username}@${instance}`
|
||||||
|
);
|
||||||
|
avatar.appendChild(avatarPicture);
|
||||||
|
comment.appendChild(avatar);
|
||||||
|
|
||||||
|
let instanceBadge = document.createElement("a");
|
||||||
|
instanceBadge.className = "instance";
|
||||||
|
instanceBadge.setAttribute("href", status.account.url);
|
||||||
|
instanceBadge.setAttribute(
|
||||||
|
"title",
|
||||||
|
`@${status.account.username}@${instance}`
|
||||||
|
);
|
||||||
|
instanceBadge.setAttribute("rel", relAttributes);
|
||||||
|
instanceBadge.textContent = instance;
|
||||||
|
|
||||||
|
let display = document.createElement("span");
|
||||||
|
display.className = "display";
|
||||||
|
display.setAttribute("itemprop", "author");
|
||||||
|
display.setAttribute("itemtype", "http://schema.org/Person");
|
||||||
|
display.innerHTML = status.account.display_name;
|
||||||
|
|
||||||
|
let header = document.createElement("header");
|
||||||
|
header.className = "author";
|
||||||
|
header.appendChild(display);
|
||||||
|
header.appendChild(instanceBadge);
|
||||||
|
comment.appendChild(header);
|
||||||
|
|
||||||
|
let permalink = document.createElement("a");
|
||||||
|
permalink.setAttribute("href", status.url);
|
||||||
|
permalink.setAttribute("itemprop", "url");
|
||||||
|
permalink.setAttribute("title", `${viewCommentText} ${instance}`);
|
||||||
|
permalink.setAttribute("rel", relAttributes);
|
||||||
|
permalink.textContent = new Date(
|
||||||
|
status.created_at
|
||||||
|
).toLocaleString(dateLocale, {
|
||||||
|
dateStyle: "long",
|
||||||
|
timeStyle: "short",
|
||||||
|
});
|
||||||
|
|
||||||
|
let timestamp = document.createElement("time");
|
||||||
|
timestamp.setAttribute("datetime", status.created_at);
|
||||||
|
timestamp.appendChild(permalink);
|
||||||
|
permalink.classList.add("external");
|
||||||
|
comment.appendChild(timestamp);
|
||||||
|
|
||||||
|
let main = document.createElement("main");
|
||||||
|
main.setAttribute("itemprop", "text");
|
||||||
|
|
||||||
|
if (status.sensitive == true || status.spoiler_text != "") {
|
||||||
|
let summary = document.createElement("summary");
|
||||||
|
if (status.spoiler_text == "") {
|
||||||
|
status.spoiler_text == sensitiveText;
|
||||||
|
}
|
||||||
|
summary.innerHTML = status.spoiler_text;
|
||||||
|
|
||||||
|
let spoiler = document.createElement("details");
|
||||||
|
spoiler.appendChild(summary);
|
||||||
|
spoiler.innerHTML += status.content;
|
||||||
|
|
||||||
|
main.appendChild(spoiler);
|
||||||
|
} else {
|
||||||
|
main.innerHTML = status.content;
|
||||||
|
}
|
||||||
|
comment.appendChild(main);
|
||||||
|
|
||||||
|
let attachments = status.media_attachments;
|
||||||
|
let SUPPORTED_MEDIA = ["image", "video", "gifv", "audio"];
|
||||||
|
let media = document.createElement("div");
|
||||||
|
media.className = "attachments";
|
||||||
|
if (
|
||||||
|
attachments &&
|
||||||
|
Array.isArray(attachments) &&
|
||||||
|
attachments.length > 0
|
||||||
|
) {
|
||||||
|
attachments.forEach((attachment) => {
|
||||||
|
if (SUPPORTED_MEDIA.includes(attachment.type)) {
|
||||||
|
|
||||||
|
let mediaElement;
|
||||||
|
switch (attachment.type) {
|
||||||
|
case "image":
|
||||||
|
mediaElement = document.createElement("img");
|
||||||
|
mediaElement.setAttribute("src", attachment.preview_url);
|
||||||
|
|
||||||
|
if (attachment.description != null) {
|
||||||
|
mediaElement.setAttribute("alt", attachment.description);
|
||||||
|
mediaElement.setAttribute("title", attachment.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lazyAsyncImage == "true") {
|
||||||
|
mediaElement.setAttribute("decoding", "async");
|
||||||
|
mediaElement.setAttribute("loading", "lazy");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.sensitive == true) {
|
||||||
|
mediaElement.classList.add("spoiler");
|
||||||
|
}
|
||||||
|
|
||||||
|
media.appendChild(mediaElement);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "video":
|
||||||
|
mediaElement = document.createElement("video");
|
||||||
|
mediaElement.setAttribute("src", attachment.url);
|
||||||
|
mediaElement.setAttribute("controls", "");
|
||||||
|
|
||||||
|
if (attachment.description != null) {
|
||||||
|
mediaElement.setAttribute("aria-title", attachment.description);
|
||||||
|
mediaElement.setAttribute("title", attachment.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.sensitive == true) {
|
||||||
|
mediaElement.classList.add("spoiler");
|
||||||
|
}
|
||||||
|
|
||||||
|
media.appendChild(mediaElement);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "gifv":
|
||||||
|
mediaElement = document.createElement("video");
|
||||||
|
mediaElement.setAttribute("src", attachment.url);
|
||||||
|
mediaElement.setAttribute("autoplay", "");
|
||||||
|
mediaElement.setAttribute("playsinline", "");
|
||||||
|
mediaElement.setAttribute("loop", "");
|
||||||
|
|
||||||
|
if (attachment.description != null) {
|
||||||
|
mediaElement.setAttribute("aria-title", attachment.description);
|
||||||
|
mediaElement.setAttribute("title", attachment.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.sensitive == true) {
|
||||||
|
mediaElement.classList.add("spoiler");
|
||||||
|
}
|
||||||
|
|
||||||
|
media.appendChild(mediaElement);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "audio":
|
||||||
|
mediaElement = document.createElement("audio");
|
||||||
|
mediaElement.setAttribute("src", attachment.url);
|
||||||
|
mediaElement.setAttribute("controls", "");
|
||||||
|
|
||||||
|
if (attachment.description != null) {
|
||||||
|
mediaElement.setAttribute("aria-title", attachment.description);
|
||||||
|
mediaElement.setAttribute("title", attachment.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
media.appendChild(mediaElement);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mediaLink = document.createElement("a");
|
||||||
|
mediaLink.setAttribute("href", attachment.url);
|
||||||
|
mediaLink.setAttribute("rel", relAttributes);
|
||||||
|
mediaLink.appendChild(mediaElement);
|
||||||
|
|
||||||
|
media.appendChild(mediaLink);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
comment.appendChild(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
let interactions = document.createElement("footer");
|
||||||
|
|
||||||
|
let boosts = document.createElement("a");
|
||||||
|
boosts.className = "boosts";
|
||||||
|
boosts.setAttribute("href", `${status.url}/reblogs`);
|
||||||
|
boosts.setAttribute("title", `${boostsFromText}`.replace("$INSTANCE", instance));
|
||||||
|
|
||||||
|
let boostsIcon = document.createElement("i");
|
||||||
|
boostsIcon.className = "icon";
|
||||||
|
boosts.appendChild(boostsIcon);
|
||||||
|
boosts.insertAdjacentHTML('beforeend', ` ${status.reblogs_count}`);
|
||||||
|
interactions.appendChild(boosts);
|
||||||
|
|
||||||
|
let faves = document.createElement("a");
|
||||||
|
faves.className = "faves";
|
||||||
|
faves.setAttribute("href", `${status.url}/favourites`);
|
||||||
|
faves.setAttribute("title", `${favesFromText}`.replace("$INSTANCE", instance));
|
||||||
|
|
||||||
|
let favesIcon = document.createElement("i");
|
||||||
|
favesIcon.className = "icon";
|
||||||
|
faves.appendChild(favesIcon);
|
||||||
|
faves.insertAdjacentHTML('beforeend', ` ${status.favourites_count}`);
|
||||||
|
interactions.appendChild(faves);
|
||||||
|
comment.appendChild(interactions);
|
||||||
|
|
||||||
|
if (status.card != null) {
|
||||||
|
let cardFigure = document.createElement("figure");
|
||||||
|
|
||||||
|
if (status.card.image != null) {
|
||||||
|
let cardImg = document.createElement("img");
|
||||||
|
cardImg.setAttribute("src", status.card.image);
|
||||||
|
cardImg.classList.add("no-hover");
|
||||||
|
cardFigure.appendChild(cardImg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cardCaption = document.createElement("figcaption");
|
||||||
|
|
||||||
|
let cardTitle = document.createElement("strong");
|
||||||
|
cardTitle.innerHTML = status.card.title;
|
||||||
|
cardCaption.appendChild(cardTitle);
|
||||||
|
|
||||||
|
if (status.card.description != null && status.card.description.length > 0) {
|
||||||
|
let cardDescription = document.createElement("p");
|
||||||
|
cardDescription.innerHTML = status.card.description;
|
||||||
|
cardCaption.appendChild(cardDescription);
|
||||||
|
}
|
||||||
|
|
||||||
|
cardFigure.appendChild(cardCaption);
|
||||||
|
|
||||||
|
let card = document.createElement("a");
|
||||||
|
card.className = "card";
|
||||||
|
card.setAttribute("href", status.card.url);
|
||||||
|
card.setAttribute("rel", relAttributes);
|
||||||
|
card.appendChild(cardFigure);
|
||||||
|
|
||||||
|
comment.appendChild(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op === true) {
|
||||||
|
comment.classList.add("op");
|
||||||
|
|
||||||
|
avatar.classList.add("op");
|
||||||
|
avatar.setAttribute(
|
||||||
|
"title",
|
||||||
|
`${blogPostAuthorText}: ` + avatar.getAttribute("title")
|
||||||
|
);
|
||||||
|
|
||||||
|
instanceBadge.classList.add("op");
|
||||||
|
instanceBadge.setAttribute(
|
||||||
|
"title",
|
||||||
|
`${blogPostAuthorText}: ` + instanceBadge.getAttribute("title")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
commentsWrapper.innerHTML += comment.outerHTML;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
var statusText = document.createElement("p");
|
||||||
|
statusText.innerHTML = noCommentsText;
|
||||||
|
statusText.setAttribute("id", "comments-status");
|
||||||
|
commentsWrapper.appendChild(statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCommentsButton.innerHTML = reloadText;
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.error('Error loading comments:', error);
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
loadCommentsButton.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
57
public/copy-button.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// Based on https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
let blocks = document.querySelectorAll("pre[class^='language-']");
|
||||||
|
|
||||||
|
blocks.forEach((block) => {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
// Code block header title
|
||||||
|
let title = document.createElement("span");
|
||||||
|
let lang = block.getAttribute("data-lang");
|
||||||
|
title.innerHTML = lang;
|
||||||
|
|
||||||
|
// Copy button icon
|
||||||
|
let icon = document.createElement("i");
|
||||||
|
icon.classList.add("icon");
|
||||||
|
|
||||||
|
// Copy button
|
||||||
|
let button = document.createElement("button");
|
||||||
|
let copyCodeText = document.getElementById("copy-code-text").textContent;
|
||||||
|
button.setAttribute("title", copyCodeText)
|
||||||
|
button.appendChild(icon);
|
||||||
|
|
||||||
|
// Code block header
|
||||||
|
let header = document.createElement("div");
|
||||||
|
header.classList.add("header");
|
||||||
|
header.appendChild(title);
|
||||||
|
header.appendChild(button);
|
||||||
|
|
||||||
|
// Container that holds header and the code block itself
|
||||||
|
let container = document.createElement("div");
|
||||||
|
container.classList.add("pre-container");
|
||||||
|
container.appendChild(header);
|
||||||
|
|
||||||
|
// Move code block into the container
|
||||||
|
block.parentNode.insertBefore(container, block);
|
||||||
|
container.appendChild(block);
|
||||||
|
|
||||||
|
button.addEventListener("click", async () => {
|
||||||
|
await copyCode(block, header, button); // Pass the button here
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function copyCode(block, header, button) {
|
||||||
|
let code = block.querySelector("code");
|
||||||
|
let text = code.innerText;
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
|
||||||
|
header.classList.add("active");
|
||||||
|
button.setAttribute("disabled", true);
|
||||||
|
|
||||||
|
header.addEventListener("animationend", () => {
|
||||||
|
header.classList.remove("active");
|
||||||
|
button.removeAttribute("disabled");
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
});
|
271
public/count.js
Normal file
|
@ -0,0 +1,271 @@
|
||||||
|
// GoatCounter: https://www.goatcounter.com
|
||||||
|
// This file is released under the ISC license: https://opensource.org/licenses/ISC
|
||||||
|
;(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
if (window.goatcounter && window.goatcounter.vars) // Compatibility with very old version; do not use.
|
||||||
|
window.goatcounter = window.goatcounter.vars
|
||||||
|
else
|
||||||
|
window.goatcounter = window.goatcounter || {}
|
||||||
|
|
||||||
|
// Load settings from data-goatcounter-settings.
|
||||||
|
var s = document.querySelector('script[data-goatcounter]')
|
||||||
|
if (s && s.dataset.goatcounterSettings) {
|
||||||
|
try { var set = JSON.parse(s.dataset.goatcounterSettings) }
|
||||||
|
catch (err) { console.error('invalid JSON in data-goatcounter-settings: ' + err) }
|
||||||
|
for (var k in set)
|
||||||
|
if (['no_onload', 'no_events', 'allow_local', 'allow_frame', 'path', 'title', 'referrer', 'event'].indexOf(k) > -1)
|
||||||
|
window.goatcounter[k] = set[k]
|
||||||
|
}
|
||||||
|
|
||||||
|
var enc = encodeURIComponent
|
||||||
|
|
||||||
|
// Get all data we're going to send off to the counter endpoint.
|
||||||
|
var get_data = function(vars) {
|
||||||
|
var data = {
|
||||||
|
p: (vars.path === undefined ? goatcounter.path : vars.path),
|
||||||
|
r: (vars.referrer === undefined ? goatcounter.referrer : vars.referrer),
|
||||||
|
t: (vars.title === undefined ? goatcounter.title : vars.title),
|
||||||
|
e: !!(vars.event || goatcounter.event),
|
||||||
|
s: [window.screen.width, window.screen.height, (window.devicePixelRatio || 1)],
|
||||||
|
b: is_bot(),
|
||||||
|
q: location.search,
|
||||||
|
}
|
||||||
|
|
||||||
|
var rcb, pcb, tcb // Save callbacks to apply later.
|
||||||
|
if (typeof(data.r) === 'function') rcb = data.r
|
||||||
|
if (typeof(data.t) === 'function') tcb = data.t
|
||||||
|
if (typeof(data.p) === 'function') pcb = data.p
|
||||||
|
|
||||||
|
if (is_empty(data.r)) data.r = document.referrer
|
||||||
|
if (is_empty(data.t)) data.t = document.title
|
||||||
|
if (is_empty(data.p)) data.p = get_path()
|
||||||
|
|
||||||
|
if (rcb) data.r = rcb(data.r)
|
||||||
|
if (tcb) data.t = tcb(data.t)
|
||||||
|
if (pcb) data.p = pcb(data.p)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a value is "empty" for the purpose of get_data().
|
||||||
|
var is_empty = function(v) { return v === null || v === undefined || typeof(v) === 'function' }
|
||||||
|
|
||||||
|
// See if this looks like a bot; there is some additional filtering on the
|
||||||
|
// backend, but these properties can't be fetched from there.
|
||||||
|
var is_bot = function() {
|
||||||
|
// Headless browsers are probably a bot.
|
||||||
|
var w = window, d = document
|
||||||
|
if (w.callPhantom || w._phantom || w.phantom)
|
||||||
|
return 150
|
||||||
|
if (w.__nightmare)
|
||||||
|
return 151
|
||||||
|
if (d.__selenium_unwrapped || d.__webdriver_evaluate || d.__driver_evaluate)
|
||||||
|
return 152
|
||||||
|
if (navigator.webdriver)
|
||||||
|
return 153
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object to urlencoded string, starting with a ?.
|
||||||
|
var urlencode = function(obj) {
|
||||||
|
var p = []
|
||||||
|
for (var k in obj)
|
||||||
|
if (obj[k] !== '' && obj[k] !== null && obj[k] !== undefined && obj[k] !== false)
|
||||||
|
p.push(enc(k) + '=' + enc(obj[k]))
|
||||||
|
return '?' + p.join('&')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show a warning in the console.
|
||||||
|
var warn = function(msg) {
|
||||||
|
if (console && 'warn' in console)
|
||||||
|
console.warn('goatcounter: ' + msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the endpoint to send requests to.
|
||||||
|
var get_endpoint = function() {
|
||||||
|
var s = document.querySelector('script[data-goatcounter]')
|
||||||
|
if (s && s.dataset.goatcounter)
|
||||||
|
return s.dataset.goatcounter
|
||||||
|
return (goatcounter.endpoint || window.counter) // counter is for compat; don't use.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current path.
|
||||||
|
var get_path = function() {
|
||||||
|
var loc = location,
|
||||||
|
c = document.querySelector('link[rel="canonical"][href]')
|
||||||
|
if (c) { // May be relative or point to different domain.
|
||||||
|
var a = document.createElement('a')
|
||||||
|
a.href = c.href
|
||||||
|
if (a.hostname.replace(/^www\./, '') === location.hostname.replace(/^www\./, ''))
|
||||||
|
loc = a
|
||||||
|
}
|
||||||
|
return (loc.pathname + loc.search) || '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run function after DOM is loaded.
|
||||||
|
var on_load = function(f) {
|
||||||
|
if (document.body === null)
|
||||||
|
document.addEventListener('DOMContentLoaded', function() { f() }, false)
|
||||||
|
else
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter some requests that we (probably) don't want to count.
|
||||||
|
goatcounter.filter = function() {
|
||||||
|
if ('visibilityState' in document && document.visibilityState === 'prerender')
|
||||||
|
return 'visibilityState'
|
||||||
|
if (!goatcounter.allow_frame && location !== parent.location)
|
||||||
|
return 'frame'
|
||||||
|
if (!goatcounter.allow_local && location.hostname.match(/(localhost$|^127\.|^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\.|^0\.0\.0\.0$)/))
|
||||||
|
return 'localhost'
|
||||||
|
if (!goatcounter.allow_local && location.protocol === 'file:')
|
||||||
|
return 'localfile'
|
||||||
|
if (localStorage && localStorage.getItem('skipgc') === 't')
|
||||||
|
return 'disabled with #toggle-goatcounter'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get URL to send to GoatCounter.
|
||||||
|
window.goatcounter.url = function(vars) {
|
||||||
|
var data = get_data(vars || {})
|
||||||
|
if (data.p === null) // null from user callback.
|
||||||
|
return
|
||||||
|
data.rnd = Math.random().toString(36).substr(2, 5) // Browsers don't always listen to Cache-Control.
|
||||||
|
|
||||||
|
var endpoint = get_endpoint()
|
||||||
|
if (!endpoint)
|
||||||
|
return warn('no endpoint found')
|
||||||
|
|
||||||
|
return endpoint + urlencode(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count a hit.
|
||||||
|
window.goatcounter.count = function(vars) {
|
||||||
|
var f = goatcounter.filter()
|
||||||
|
if (f)
|
||||||
|
return warn('not counting because of: ' + f)
|
||||||
|
var url = goatcounter.url(vars)
|
||||||
|
if (!url)
|
||||||
|
return warn('not counting because path callback returned null')
|
||||||
|
|
||||||
|
if (!navigator.sendBeacon(url)) {
|
||||||
|
// This mostly fails due to being blocked by CSP; try again with an
|
||||||
|
// image-based fallback.
|
||||||
|
var img = document.createElement('img')
|
||||||
|
img.src = url
|
||||||
|
img.style.position = 'absolute' // Affect layout less.
|
||||||
|
img.style.bottom = '0px'
|
||||||
|
img.style.width = '1px'
|
||||||
|
img.style.height = '1px'
|
||||||
|
img.loading = 'eager'
|
||||||
|
img.setAttribute('alt', '')
|
||||||
|
img.setAttribute('aria-hidden', 'true')
|
||||||
|
|
||||||
|
var rm = function() { if (img && img.parentNode) img.parentNode.removeChild(img) }
|
||||||
|
img.addEventListener('load', rm, false)
|
||||||
|
document.body.appendChild(img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a query parameter.
|
||||||
|
window.goatcounter.get_query = function(name) {
|
||||||
|
var s = location.search.substr(1).split('&')
|
||||||
|
for (var i = 0; i < s.length; i++)
|
||||||
|
if (s[i].toLowerCase().indexOf(name.toLowerCase() + '=') === 0)
|
||||||
|
return s[i].substr(name.length + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track click events.
|
||||||
|
window.goatcounter.bind_events = function() {
|
||||||
|
if (!document.querySelectorAll) // Just in case someone uses an ancient browser.
|
||||||
|
return
|
||||||
|
|
||||||
|
var send = function(elem) {
|
||||||
|
return function() {
|
||||||
|
goatcounter.count({
|
||||||
|
event: true,
|
||||||
|
path: (elem.dataset.goatcounterClick || elem.name || elem.id || ''),
|
||||||
|
title: (elem.dataset.goatcounterTitle || elem.title || (elem.innerHTML || '').substr(0, 200) || ''),
|
||||||
|
referrer: (elem.dataset.goatcounterReferrer || elem.dataset.goatcounterReferral || ''),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.prototype.slice.call(document.querySelectorAll("*[data-goatcounter-click]")).forEach(function(elem) {
|
||||||
|
if (elem.dataset.goatcounterBound)
|
||||||
|
return
|
||||||
|
var f = send(elem)
|
||||||
|
elem.addEventListener('click', f, false)
|
||||||
|
elem.addEventListener('auxclick', f, false) // Middle click.
|
||||||
|
elem.dataset.goatcounterBound = 'true'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a "visitor counter" frame or image.
|
||||||
|
window.goatcounter.visit_count = function(opt) {
|
||||||
|
on_load(function() {
|
||||||
|
opt = opt || {}
|
||||||
|
opt.type = opt.type || 'html'
|
||||||
|
opt.append = opt.append || 'body'
|
||||||
|
opt.path = opt.path || get_path()
|
||||||
|
opt.attr = opt.attr || {width: '200', height: (opt.no_branding ? '60' : '80')}
|
||||||
|
|
||||||
|
opt.attr['src'] = get_endpoint() + 'er/' + enc(opt.path) + '.' + enc(opt.type) + '?'
|
||||||
|
if (opt.no_branding) opt.attr['src'] += '&no_branding=1'
|
||||||
|
if (opt.style) opt.attr['src'] += '&style=' + enc(opt.style)
|
||||||
|
if (opt.start) opt.attr['src'] += '&start=' + enc(opt.start)
|
||||||
|
if (opt.end) opt.attr['src'] += '&end=' + enc(opt.end)
|
||||||
|
|
||||||
|
var tag = {png: 'img', svg: 'img', html: 'iframe'}[opt.type]
|
||||||
|
if (!tag)
|
||||||
|
return warn('visit_count: unknown type: ' + opt.type)
|
||||||
|
|
||||||
|
if (opt.type === 'html') {
|
||||||
|
opt.attr['frameborder'] = '0'
|
||||||
|
opt.attr['scrolling'] = 'no'
|
||||||
|
}
|
||||||
|
|
||||||
|
var d = document.createElement(tag)
|
||||||
|
for (var k in opt.attr)
|
||||||
|
d.setAttribute(k, opt.attr[k])
|
||||||
|
|
||||||
|
var p = document.querySelector(opt.append)
|
||||||
|
if (!p)
|
||||||
|
return warn('visit_count: append not found: ' + opt.append)
|
||||||
|
p.appendChild(d)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make it easy to skip your own views.
|
||||||
|
if (location.hash === '#toggle-goatcounter') {
|
||||||
|
if (localStorage.getItem('skipgc') === 't') {
|
||||||
|
localStorage.removeItem('skipgc', 't')
|
||||||
|
alert('GoatCounter tracking is now ENABLED in this browser.')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
localStorage.setItem('skipgc', 't')
|
||||||
|
alert('GoatCounter tracking is now DISABLED in this browser until ' + location + ' is loaded again.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!goatcounter.no_onload)
|
||||||
|
on_load(function() {
|
||||||
|
// 1. Page is visible, count request.
|
||||||
|
// 2. Page is not yet visible; wait until it switches to 'visible' and count.
|
||||||
|
// See #487
|
||||||
|
if (!('visibilityState' in document) || document.visibilityState === 'visible')
|
||||||
|
goatcounter.count()
|
||||||
|
else {
|
||||||
|
var f = function(e) {
|
||||||
|
if (document.visibilityState !== 'visible')
|
||||||
|
return
|
||||||
|
document.removeEventListener('visibilitychange', f)
|
||||||
|
goatcounter.count()
|
||||||
|
}
|
||||||
|
document.addEventListener('visibilitychange', f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!goatcounter.no_events)
|
||||||
|
goatcounter.bind_events()
|
||||||
|
})
|
||||||
|
})();
|
48
public/css/gallery.css
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
#image-gallery {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
column-count: 3;
|
||||||
|
column-gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
break-inside: avoid;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
list-style: none; /* ← important! */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img:hover {
|
||||||
|
transform: scale(1.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0; /* ← just in case */
|
||||||
|
color: var(--fg-color);
|
||||||
|
list-style: none; /* ← extra-safe */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.gallery {
|
||||||
|
column-count: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.gallery {
|
||||||
|
column-count: 1;
|
||||||
|
}
|
||||||
|
}
|
28
public/css/mermaid.css
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
.mermaid {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
background-color: var(--code-bg);
|
||||||
|
font-family: var(--code-font, monospace);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid svg {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.mermaid {
|
||||||
|
background-color: var(--code-bg-dark, #2d2d2d);
|
||||||
|
}
|
||||||
|
}
|
72
public/css/skills.css
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/* Basic Layout for Skills List */
|
||||||
|
#skills-content {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#skills-content .category {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#skills-content .category h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: regular;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skills container for single-line display */
|
||||||
|
#skills-content .skills-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex; /* Use flexbox for single-line display */
|
||||||
|
flex-wrap: wrap; /* Allow skills to wrap if needed */
|
||||||
|
gap: 1rem; /* Spacing between skills */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skill List Item */
|
||||||
|
#skills-content .skills-list .skill {
|
||||||
|
display: inline-flex; /* Ensures each skill is aligned inline */
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer; /* Show pointer cursor on hover */
|
||||||
|
text-decoration: none; /* Remove underline from links */
|
||||||
|
color: inherit; /* Inherit color from parent */
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Skill Name */
|
||||||
|
#skills-content .skills-list .skill span {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skill Icon */
|
||||||
|
#skills-content .skills-list .skill .skill-icon {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
color: var(--accent-color);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover Effects for Skills */
|
||||||
|
#skills-content .skills-list .skill:hover::before {
|
||||||
|
background: var(--accent-color-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
#skills-content .skills-list .skill:hover {
|
||||||
|
background-color: var(--accent-color-alpha);
|
||||||
|
color: var(--fg-color); /* Change text color on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skill Category Hover Effect */
|
||||||
|
#skills-content .skills-list .skill:hover span {
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: Customize Icon Color on Hover */
|
||||||
|
#skills-content .skills-list .skill:hover .skill-icon {
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
91
public/css/timeline.css
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/* Basic Layout */
|
||||||
|
#timeline-content {
|
||||||
|
position: relative;
|
||||||
|
margin: 2rem 0;
|
||||||
|
padding-left: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#timeline-content ul.timeline {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#timeline-content ul.timeline::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -30px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event List Item */
|
||||||
|
#timeline-content li.event {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event Circle */
|
||||||
|
#timeline-content li.event::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -39px;
|
||||||
|
top: 5px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 2px solid white;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* From + To Label — styled exactly like old date label */
|
||||||
|
#timeline-content li.event::after {
|
||||||
|
content: attr(data-from) "\A" attr(data-to);
|
||||||
|
white-space: pre; /* ensures newline works */
|
||||||
|
position: absolute;
|
||||||
|
left: -160px;
|
||||||
|
width: 100px;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--fg-color);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event Heading */
|
||||||
|
#timeline-content li.event h3 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event Description */
|
||||||
|
#timeline-content li.event p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Timeline Icon */
|
||||||
|
#timeline-content .timeline-icon {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
color: var(--accent-color);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover Effects */
|
||||||
|
#timeline-content li.event:hover::before {
|
||||||
|
background: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#timeline-content li.event:hover {
|
||||||
|
background-color: var(--accent-color-alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* From + To Label Hover Effect */
|
||||||
|
#timeline-content li.event:hover::after {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
30
public/data/skills.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"grouping": "Languages, Operating Systems & Tools",
|
||||||
|
"skills": [
|
||||||
|
{ "name": "Cplusplus", "icon": "fa-solid fa-c" },
|
||||||
|
{ "name": "Rust", "icon": "fab fa-rust" },
|
||||||
|
{ "name": "Java", "icon": "fab fa-java" },
|
||||||
|
{ "name": "Python", "icon": "fab fa-python" },
|
||||||
|
{ "name": "git", "icon": "fas fa-code-branch" },
|
||||||
|
{ "name": "linux", "icon": "fab fa-linux" },
|
||||||
|
{ "name": "bash", "icon": "fas fa-terminal" },
|
||||||
|
{ "name": "javascript", "icon": "fab fa-js" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"grouping": "Platform Development & Administration",
|
||||||
|
"skills": [
|
||||||
|
{ "name": "NGINX", "icon": "fas fa-server" },
|
||||||
|
{ "name": "MySQL", "icon": "fas fa-database" },
|
||||||
|
{ "name": "Slurm", "icon": "fas fa-project-diagram" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"grouping": "Containers & Cloud",
|
||||||
|
"skills": [
|
||||||
|
{ "name": "Docker", "icon": "fab fa-docker" },
|
||||||
|
{ "name": "Aliyun", "icon": "fas fa-cloud" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
3904
public/de/atom.xml
Normal file
45
public/de/index.html
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<!doctype html><html data-theme=light lang=de xmlns=http://www.w3.org/1999/xhtml><head><meta charset=UTF-8><meta content="Mein Portfolio, Blog und allgemeine Präsenz online" name=description><meta content="width=device-width,initial-scale=1" name=viewport><meta content=#FF7E3C name=theme-color><meta content=#FF7E3C media=(prefers-color-scheme:dark) name=theme-color><title>Aron Petau</title><link href=https://aron.petau.net/de/ rel=canonical><link href=https://mastodon.online/@reprintedAron rel=me><meta content=@reprintedAron@mastodon.online name=fediverse:creator><link href=https://aron.petau.net/favicon.png rel=icon type=image/png><link href=https://aron.petau.net/apple-touch-icon.png rel=apple-touch-icon sizes=180x180 type=image/png><link crossorigin href=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css integrity=sha512-... referrerpolicy=no-referrer rel=stylesheet><link title="Aron Petau - Atom Feed" href=https://aron.petau.net/atom.xml rel=alternate type=application/atom+xml><style>:root,[data-theme=dark]{--accent-color:#ff7e3c}@media (prefers-color-scheme:dark){:root:not([data-theme=light]){--accent-color:#ff7e3c}}</style><link href=https://aron.petau.net/style.css rel=stylesheet><link href=https://aron.petau.net/css/timeline.css rel=stylesheet><link href=https://aron.petau.net/css/mermaid.css rel=stylesheet><link href=https://aron.petau.net/css/skills.css rel=stylesheet><link href=https://aron.petau.net/css/gallery.css rel=stylesheet><script defer src=https://aron.petau.net/closable.js></script><script defer src=https://aron.petau.net/copy-button.js></script><script data-goatcounter=https://duckquill.goatcounter.com/count defer src=https://aron.petau.net/count.js></script><script defer src=https://aron.petau.net/fuse.js></script><script defer src=https://aron.petau.net/search-fuse.js></script><script defer src=https://aron.petau.net/theme-switcher.js></script><script type=module>import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
||||||
|
mermaid.initialize({ startOnLoad: true });</script><meta content="Aron Petau" property=og:site_name><meta content="Aron Petau" property=og:title><meta content=https://aron.petau.net/de/ property=og:url><meta content="Mein Portfolio, Blog und allgemeine Präsenz online" property=og:description><meta content=https://aron.petau.net/card.png property=og:image><meta content=de_DE property=og:locale><body><header id=site-nav><nav><a href=#main-content tabindex=0> Zum Hauptinhalt springen </a><ul><li id=home><a class=active href=https://aron.petau.net/de/> <i class=icon></i>Aron Petau</a><li class=divider><li><a href=https://aron.petau.net/de/project/>Projekte</a><li><a href=https://aron.petau.net/de/pages/contact/>Kontakt</a><li><a href=https://aron.petau.net/de/pages/cv/>Vita</a><li><a href=https://aron.petau.net/de/pages/about/>Über mich</a><li id=search><button class=circle id=search-toggle title=Suche><i class=icon></i></button><li id=language-switcher><details class=closable><summary class=circle title=Sprache><i class=icon></i></summary> <ul><li><a href=https://aron.petau.net/ lang=en>English</a></ul></details><li id=theme-switcher><details class=closable><summary class=circle title=Thema><i class=icon></i></summary> <ul><li><button title="Zum hellen Thema wechseln" class=circle id=theme-light><i class=icon></i></button><li><button title="Zum dunklen Thema wechseln" class=circle id=theme-dark><i class=icon></i></button><li><button title="Systemthema nutzen" class=circle id=theme-system><i class=icon></i></button></ul></details><li id=feed><a class=circle href=https://aron.petau.net/de/atom.xml title=Feed> <i class=icon></i> </a><li id=repo><a class=circle href=https://forgejo.petau.net/aron/awebsite title=Repository> <i class=icon></i> </a></ul></nav><div id=search-container><label class=visually-hidden for=search-bar>Suche</label><input placeholder="Suche nach…" autocomplete=off disabled id=search-bar type=search><div id=search-results-container><div id=search-results></div></div></div></header><main id=main-content><h2 id=Willkommen><a aria-label="Anchor link for: Willkommen" class=zola-anchor href=#Willkommen><i class=icon></i></a> Willkommen</h2><p>auf der Online-Präsenz von Aron Petau.<aside><p><img title="Aron Petau" alt=logo src=/images/logo.png></aside><p>Ich verwende die Pronomen er/ihm und lebe in Berlin, Deutschland.<p>Ich bin Tüftler, Designer, Softwareentwickler und arbeite in der Forschung zu digitaler Bildung.<p>Diese Seite ist eine Sammlung meiner Gedanken und Erfahrungen.<p>Ich hoffe, du findest hier etwas Interessantes.<p><blockquote class=note><p class=alert-title><i class=icon></i>Anmerkung<p>Diese Seite befindet sich derzeit im aktiven Umbau. Fehlende oder kaputte Links sind zu erwarten.<p>Solange der Umzug bzw. das Redesign nicht vollständig abgeschlossen ist, ist die alte Seite weiterhin hier erreichbar: <a href=https://old.aron.petau.net>old.aron.petau.net</a></blockquote> Fortschritt des Umbaus: <progress max=100 value=90></progress><p><blockquote class=note><p class=alert-title><i class=icon></i>Anmerkung<p>Außerdem gibt es erste Bemühungen, diese Website zu übersetzen. Das ist ein ziemlich aufwändiger Prozess und wird einige Zeit dauern.</blockquote> Fortschritt der Übersetzung: <progress max=100 value=15></progress><blockquote class=important><p class=alert-title><i class=icon></i>Wichtig<p>Zuletzt aktualisiert: 2025-05-14</blockquote><div class="crt scanlines" aria-hidden=true><pre><code>➜ content git:(main) ✗ tree -L 2
|
||||||
|
.
|
||||||
|
├── _index.md
|
||||||
|
├── blog
|
||||||
|
│ ├── _index.md
|
||||||
|
│ ├── 2018-05-03-printing
|
||||||
|
│ ├── 2018-07-05-cad
|
||||||
|
│ ├── 2018-09-01-beacon
|
||||||
|
│ ├── 2019-03-19-plastic-recycling
|
||||||
|
│ ├── 2019-06-01-ballpark
|
||||||
|
│ ├── 2020-03-01-homebrew
|
||||||
|
│ ├── 2020-07-14-critical-epistemologies
|
||||||
|
│ ├── 2020-07-15-chatbot
|
||||||
|
│ ├── 2021-03-01-coding
|
||||||
|
│ ├── 2021-03-01-philosophy
|
||||||
|
│ ├── 2021-04-13-thesis
|
||||||
|
│ ├── 2021-08-01-iron-smelting
|
||||||
|
│ ├── 2021-12-05-political-violence
|
||||||
|
│ ├── 2022-01-22-critical-philosophy-subjectivity
|
||||||
|
│ ├── 2022-04-01-allei
|
||||||
|
│ ├── 2022-04-30-lusatia
|
||||||
|
│ ├── 2022-12-03-stable-dreamfusion
|
||||||
|
│ ├── 2022-12-04-lampshades
|
||||||
|
│ ├── 2023-01-03-auraglow
|
||||||
|
│ ├── 2023-03-01-ruminations
|
||||||
|
│ ├── 2023-06-16-ascendancy
|
||||||
|
│ ├── 2023-06-20-autoimmunitaet
|
||||||
|
│ ├── 2023-06-20-dreams-of-cars
|
||||||
|
│ ├── 2023-12-06-postmaster
|
||||||
|
│ ├── 2023-12-07-commoning-cars
|
||||||
|
│ ├── 2024-01-30-airaspi-build-log
|
||||||
|
│ ├── 2024-03-25-aethercomms
|
||||||
|
│ ├── 2024-04-11-local-diffusion
|
||||||
|
│ ├── 2024-04-25-echoing-dimensions
|
||||||
|
│ ├── 2024-06-20-sferics
|
||||||
|
│ ├── 2024-07-05-käsewerkstatt
|
||||||
|
│ └── 2025-04-15-master-thesis
|
||||||
|
└── pages
|
||||||
|
├── about.md
|
||||||
|
├── contact.md
|
||||||
|
├── cv.md
|
||||||
|
├── privacy.md
|
||||||
|
└── rent-ulli.md
|
||||||
|
</code></pre></div><span class=hidden id=copy-code-text>Code kopieren</span><span class=hidden id=search-index>https://aron.petau.net/de//search_index.en.json</span><span class=hidden id=more-matches-text>$MATCHES mehr Treffer</span></main><footer id=site-footer><div class=carbonbadge id=wcb></div><script defer src=https://unpkg.com/website-carbon-badges@1.1.3/b.min.js></script><nav><ul><li><a href=https://aron.petau.net/de/project/>Projekte</a><li><a href=https://aron.petau.net/de/pages/privacy/>Privacy</a><li><a class=external href=https://kaesewerkstatt.petau.net>Käsewerkstatt</a><li><a class=external href=https://www.newpractice.net/author/aron-petau>New Practice Network</a></ul></nav><p>© Aron Petau, 2025<ul id=socials><li><a rel=" me" href=https://github.com/arontaupe title=GitHub> <i style="--icon:url("data:image/svg+xml,%3Csvg role='img' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3EGitHub%3C/title%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")" class=icon></i> <span>GitHub</span> </a><li><a rel=" me" href=https://www.printables.com/@arontaupe title=Printables> <i class=icon style=--icon:url(data:image/svg+xml,%3Csvg%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EPrintables%3C%2Ftitle%3E%3Cpath%20d%3D%22M3.678%204.8%2012%209.6v9.6l8.322-4.8V4.8L12%200ZM12%2019.2l-8.322-4.8V24Z%22%2F%3E%3C%2Fsvg%3E)></i> <span>Printables</span> </a><li><a rel=" me" href=https://www.etsy.com/de-en/shop/reprintedservices title=Etsy> <i class=icon style=--icon:url(data:image/svg+xml,%3Csvg%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EEtsy%3C%2Ftitle%3E%3Cpath%20d%3D%22M8.559%202.445c0-.325.033-.52.59-.52h7.465c1.3%200%202.02%201.11%202.54%203.193l.42%201.666h1.27c.23-4.728.43-6.784.43-6.784s-3.196.36-5.09.36H6.635L1.521.196v1.37l1.725.326c1.21.24%201.5.496%201.6%201.606%200%200%20.11%203.27.11%208.64%200%205.385-.09%208.61-.09%208.61%200%20.973-.39%201.333-1.59%201.573l-1.722.33V24l5.13-.165h8.55c1.935%200%206.39.165%206.39.165.105-1.17.75-6.48.855-7.064h-1.2l-1.284%202.91c-1.005%202.28-2.476%202.445-4.11%202.445h-4.906c-1.63%200-2.415-.64-2.415-2.05V12.8s3.62%200%204.79.096c.912.064%201.463.325%201.76%201.598l.39%201.695h1.41l-.09-4.278.192-4.305h-1.391l-.45%201.89c-.283%201.244-.48%201.47-1.754%201.6-1.666.17-4.815.14-4.815.14V2.45h-.05z%22%2F%3E%3C%2Fsvg%3E)></i> <span>Etsy</span> </a><li><a rel=" me" href=https://mastodon.online/@reprintedAron title=Mastodon> <i style="--icon:url("data:image/svg+xml,%3Csvg role='img' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3EMastodon%3C/title%3E%3Cpath d='M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z'/%3E%3C/svg%3E")" class=icon></i> <span>Mastodon</span> </a></ul></footer>
|
2
public/de/pages/about/index.html
Normal file
2
public/de/pages/contact/index.html
Normal file
2
public/de/pages/cv/index.html
Normal file
2
public/de/pages/privacy/index.html
Normal file
2
public/de/pages/rent-ulli/index.html
Normal file
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 65 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 59 KiB |
BIN
public/de/project/aethercomms/aethercomms_lineart.jpg
Normal file
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 3.1 MiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 4 MiB |
After Width: | Height: | Size: 3.2 MiB |
After Width: | Height: | Size: 2.6 MiB |
After Width: | Height: | Size: 4.4 MiB |
After Width: | Height: | Size: 2.7 MiB |
After Width: | Height: | Size: 3.9 MiB |
After Width: | Height: | Size: 3.4 MiB |
After Width: | Height: | Size: 2.8 MiB |
After Width: | Height: | Size: 3.8 MiB |
After Width: | Height: | Size: 2.8 MiB |
BIN
public/de/project/aethercomms/gqrx_macos.png
Normal file
After Width: | Height: | Size: 182 KiB |
2
public/de/project/aethercomms/index.html
Normal file
After Width: | Height: | Size: 13 MiB |
After Width: | Height: | Size: 12 MiB |
After Width: | Height: | Size: 12 MiB |
After Width: | Height: | Size: 9 MiB |
After Width: | Height: | Size: 9.9 MiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 2 MiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 2.1 MiB |
After Width: | Height: | Size: 1.4 MiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 1.4 MiB |
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 721 KiB |
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 2.4 MiB |
After Width: | Height: | Size: 2.3 MiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 1.6 MiB |
After Width: | Height: | Size: 2 MiB |
After Width: | Height: | Size: 975 KiB |
After Width: | Height: | Size: 519 KiB |
After Width: | Height: | Size: 1 MiB |
After Width: | Height: | Size: 812 KiB |
After Width: | Height: | Size: 955 KiB |
After Width: | Height: | Size: 1.9 MiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 1.4 MiB |
BIN
public/de/project/aethercomms/technikmuseum/technikmuseum_1.jpeg
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
public/de/project/aethercomms/technikmuseum/technikmuseum_2.jpeg
Normal file
After Width: | Height: | Size: 99 KiB |
BIN
public/de/project/aethercomms/technikmuseum/technikmuseum_3.jpeg
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
public/de/project/aethercomms/technikmuseum/technikmuseum_4.jpeg
Normal file
After Width: | Height: | Size: 64 KiB |
152
public/de/project/airaspi-build-log/index.html
Normal file
BIN
public/de/project/allei/allei_screenshot.png
Normal file
After Width: | Height: | Size: 32 KiB |
2
public/de/project/allei/index.html
Normal file
BIN
public/de/project/ascendancy/ascendancy.jpg
Normal file
After Width: | Height: | Size: 923 KiB |
45
public/de/project/ascendancy/index.html
Normal file
BIN
public/de/project/auraglow/cage_closeup.jpeg
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
public/de/project/auraglow/cage_closeup_2.jpeg
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
public/de/project/auraglow/cage_milli.jpeg
Normal file
After Width: | Height: | Size: 69 KiB |