folder script
| 
		 Before Width: | Height: | Size: 273 KiB After Width: | Height: | Size: 273 KiB  | 
| 
		 Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB  | 
| 
		 Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/404.gif
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4 KiB  | 
							
								
								
									
										
											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  | 
							
								
								
									
										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/blog/beacon/india_key_monastery.jpg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.3 MiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/blog/cad/render_bike_holder.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 273 KiB  | 
							
								
								
									
										21
									
								
								public/blog/organize.sh
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Regex pattern for ISO date format filenames like 2019-06-01-something.md
 | 
				
			||||||
 | 
					pattern='^[0-9]{4}-[0-9]{2}-[0-9]{2}-.+\.md$'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo "Processing markdown files in $(pwd)"
 | 
				
			||||||
 | 
					echo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					for file in *.md; do
 | 
				
			||||||
 | 
					  # Skip if no matching files (glob doesn't find anything)
 | 
				
			||||||
 | 
					  [ -e "$file" ] || continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if [[ "$file" =~ $pattern ]]; then
 | 
				
			||||||
 | 
					    basename="${file%.md}"
 | 
				
			||||||
 | 
					    mkdir -p "$basename"
 | 
				
			||||||
 | 
					    mv "$file" "$basename/index.md"
 | 
				
			||||||
 | 
					    echo "✔ Processed: $file  →  $basename/index.md"
 | 
				
			||||||
 | 
					  else
 | 
				
			||||||
 | 
					    echo "✘ Skipped (pattern mismatch): $file"
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
 | 
					done
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								public/blog/plastic-recycling/recycling_graphic.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 28 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/blog/printing/cloning_station.jpg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 212 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/blog/printing/prusa.jpg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 328 KiB  | 
							
								
								
									
										
											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()
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
							
								
								
									
										22
									
								
								public/css/gallery.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					.gallery {
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.gallery a {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.gallery img {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: auto;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.caption {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  font-size: 0.9rem;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					  margin-top: 0.25rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										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);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										39
									
								
								public/css/skills.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,39 @@
 | 
				
			||||||
 | 
					.skills {
 | 
				
			||||||
 | 
					  margin-bottom: 3rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.skills-title {
 | 
				
			||||||
 | 
					  font-size: 2rem;
 | 
				
			||||||
 | 
					  font-weight: 700;
 | 
				
			||||||
 | 
					  margin-bottom: 1.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.skills-subtitle {
 | 
				
			||||||
 | 
					  font-size: 1.25rem;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  color: #444;
 | 
				
			||||||
 | 
					  margin-top: 2rem;
 | 
				
			||||||
 | 
					  margin-bottom: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.skills-list {
 | 
				
			||||||
 | 
					  list-style: none;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
 | 
					  gap: 0.75rem 1.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.skills-item {
 | 
				
			||||||
 | 
					  display: inline-flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  font-size: 1rem;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.skills-item i {
 | 
				
			||||||
 | 
					  margin-right: 0.3rem;
 | 
				
			||||||
 | 
					  font-size: 1rem;
 | 
				
			||||||
 | 
					  opacity: 0.7;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										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(--accent-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: #ffffff;
 | 
				
			||||||
 | 
					  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-dark);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#timeline-content li.event:hover {
 | 
				
			||||||
 | 
					  background-color: var(--accent-color-alpha);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* From + To Label Hover Effect */
 | 
				
			||||||
 | 
					#timeline-content li.event:hover::after {
 | 
				
			||||||
 | 
					  color: var(--fg-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" }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								public/documents/AronPetauBAThesis.pdf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/documents/Human_Waste_MA_Aron_Petau.pdf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/documents/Info_Sheet_Commoning_Cars.pdf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/favicon.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 548 B  | 
							
								
								
									
										1
									
								
								public/fonts.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					@font-face{font-style:normal;font-weight:100 900;src:url("fonts/InterVariable.woff2") format("woff2");font-family:"Inter Variable";font-display:swap}@font-face{font-style:italic;font-weight:100 900;src:url("fonts/InterVariable-Italic.woff2") format("woff2");font-family:"Inter Variable";font-display:swap}@font-face{font-style:normal;font-weight:100 900;src:url("fonts/JetBrainsMono.woff2") format("woff2");font-family:"JetBrains Mono";font-display:swap}@font-face{font-style:italic;font-weight:100 900;src:url("fonts/JetBrainsMono-Italic.woff2") format("woff2");font-family:"JetBrains Mono";font-display:swap}body{font-family:"Inter Variable",var(--font-system-ui),var(--font-emoji)}h1,h2,h3,h4,h5,h6{font-weight:bold;font-family:"Inter Variable",var(--font-system-ui),var(--font-emoji)}h1{font-weight:900}pre,code,kbd,samp{font-family:"JetBrains Mono",var(--font-monospace-code)}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								public/fonts/InterVariable-Italic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/InterVariable.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/JetBrainsMono-Italic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/JetBrainsMono.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_AMS-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Caligraphic-Bold.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Caligraphic-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Fraktur-Bold.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Fraktur-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Main-Bold.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Main-BoldItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Main-Italic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Main-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Math-BoldItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Math-Italic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_SansSerif-Bold.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_SansSerif-Italic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_SansSerif-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Script-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Size1-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Size2-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Size3-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Size4-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/fonts/KaTeX_Typewriter-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										9
									
								
								public/fuse.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/images/Key_Monastery_Spiti.png
									
										
									
									
									
										Executable file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 67 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/images/about_header.jpeg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 55 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/images/acc_sj_by_cond_distort.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 27 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/images/aethercomms/aether_screens/aether_screens_1.jpeg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 58 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/images/aethercomms/aether_screens/aether_screens_2.jpeg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 64 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/images/aethercomms/aether_screens/aether_screens_3.jpeg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 65 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/images/aethercomms/aether_screens/aether_screens_4.jpeg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 60 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/images/aethercomms/aether_screens/aether_screens_5.jpeg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 57 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/images/aethercomms/aether_screens/aether_screens_6.jpeg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 59 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/images/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  |