My personal website
1import { type ComponentChildren } from "preact";
2import { useCallback, useEffect, useRef, useState } from "preact/hooks";
3import { Footer } from "./Footer";
4import { ConsoleSpinner } from "./ConsoleSpinner";
5import { Avatar } from "./Avatar";
6import "../global.css";
7import "../scss/application.scss";
8
9const Project = ({
10 title,
11 imgPath,
12 children,
13 websiteHref,
14 sourceHref,
15}: {
16 title: string;
17 imgPath: string;
18 children: ComponentChildren;
19 websiteHref?: string;
20 sourceHref?: string;
21}) => {
22 return (
23 <div className="flex flex-col justify-between">
24 <div>
25 <img
26 src={imgPath}
27 alt={`${title} Logo`}
28 title={title}
29 loading="lazy"
30 className="mx-auto h-20"
31 />
32 <h3 className="font-display text-3xl text-center">{title}</h3>
33 {children}
34 </div>
35 <div>
36 {websiteHref && (
37 <a
38 href={websiteHref}
39 target="_blank"
40 rel="noopener"
41 className="btn btn-primary font-display"
42 >
43 <i class="icon-new_tab" /> Website
44 </a>
45 )}
46 {sourceHref && (
47 <a
48 href={sourceHref}
49 target="_blank"
50 rel="noopener"
51 className="btn btn-accent font-display"
52 >
53 <i class="icon-new_tab" /> Source
54 </a>
55 )}
56 </div>
57 </div>
58 );
59};
60
61const ContactTile = ({
62 title,
63 iconName,
64 children,
65 href,
66 buttonText,
67}: {
68 title: string;
69 iconName: string;
70 children: ComponentChildren;
71 href: string;
72 buttonText: string;
73}) => {
74 return (
75 <div className="flex flex-col justify-between">
76 <div>
77 <h3 className="font-display text-3xl text-center">
78 <i className={`icon-${iconName}`} /> {title}
79 </h3>
80 {children}
81 </div>
82 <div>
83 <a
84 href={href}
85 target="_blank"
86 rel="noopener"
87 className="btn btn-primary font-display"
88 >
89 {buttonText}
90 </a>
91 </div>
92 </div>
93 );
94};
95
96export const App = () => {
97 const developmentLinks = [
98 {
99 iconName: "codeberg",
100 title: "Codeberg",
101 href: "https://codeberg.org/Scrumplex",
102 },
103 {
104 iconName: "github",
105 title: "GitHub",
106 href: "https://github.com/Scrumplex",
107 },
108 {
109 iconName: "gitlab",
110 title: "GitLab.com",
111 href: "https://gitlab.com/Scrumplex",
112 },
113 ];
114
115 const donateLinks = [
116 {
117 iconName: "github_sponsors",
118 title: "GitHub Sponsors",
119 href: "https://github.com/sponsors/Scrumplex",
120 },
121 {
122 iconName: "liberapay",
123 title: "Liberapay",
124 href: "https://liberapay.com/Scrumplex/donate",
125 },
126 {
127 iconName: "paypal",
128 title: "PayPal",
129 href: "https://www.paypal.me/Scrumplex",
130 },
131 {
132 iconName: "ko-fi",
133 title: "Ko-Fi",
134 href: "https://ko-fi.com/scrumplex",
135 },
136 ];
137
138 const mainRef = useRef<HTMLDivElement>(null);
139
140 const [mainOffset, setMainOffset] = useState(0);
141
142 const scrollToElement = (selector: string) => {
143 const elem = document.querySelector(selector);
144 if (!elem) {
145 return;
146 }
147
148 const rect = elem.getBoundingClientRect();
149
150 const offset = window.pageYOffset + rect.top;
151
152 window.scrollTo({ top: offset, behavior: "smooth" });
153 };
154
155 const expandConditionally = useCallback(
156 (force?: boolean) => {
157 if (mainOffset > 0) {
158 return;
159 }
160
161 const rect = mainRef.current!.getBoundingClientRect();
162 if (
163 rect.top <= 0 || // offset to top window border
164 rect.height >= window.innerHeight ||
165 force
166 ) {
167 setMainOffset(window.pageYOffset + rect.top);
168 }
169 },
170 [mainRef],
171 );
172
173 useEffect(() => {
174 if (mainOffset || !mainRef.current) {
175 return;
176 }
177
178 const onScroll = () => expandConditionally();
179 window.addEventListener("scroll", onScroll);
180
181 const onHashChange = () => {
182 expandConditionally(true);
183 setTimeout(() => scrollToElement(location.hash), 10);
184 };
185 window.addEventListener("hashchange", onHashChange);
186 return () => {
187 window.removeEventListener("scroll", onScroll);
188 window.removeEventListener("hashchange", onHashChange);
189 };
190 }, [mainRef, mainOffset, setMainOffset]);
191
192 return (
193 <>
194 <noscript>
195 Enable JavaScript for best experience. Source code of this
196 website is available at{" "}
197 <a
198 href="https://codeberg.org/Scrumplex/website"
199 target="_blank"
200 rel="noopener"
201 >
202 Codeberg
203 </a>
204 .
205 </noscript>
206 <div className="container wrapper mx-auto grow" id="wrapper">
207 <div
208 ref={mainRef}
209 className={`sheet ${!mainOffset ? "sheet-splash" : "sheet-splashed"} wavy`}
210 style={mainOffset ? { marginTop: mainOffset } : {}}
211 onAnimationEnd={() => {
212 if (location.hash) {
213 expandConditionally(true);
214 location.href = location.hash;
215 }
216 }}
217 >
218 <div className="row">
219 <div className="col-med-5 text-center flex flex-col justify-between">
220 <div className="row">
221 <div className="col flex flex-col items-center">
222 <Avatar />
223 <h1 className="font-display text-5xl">
224 Scrumplex
225 </h1>
226 <strong>he/him or any</strong>
227 </div>
228 </div>
229 <div className="row">
230 <div className="col-med-6">
231 <h2 className="font-display text-4xl">
232 Development
233 </h2>
234 {developmentLinks.map((link) => (
235 <a
236 href={link.href}
237 target="_blank"
238 rel="noopener"
239 className={`link link-${link.iconName}`}
240 title={link.title}
241 >
242 <i
243 className={`icon-2x icon-${link.iconName}`}
244 />
245 </a>
246 ))}
247 </div>
248 <div className="col-med-6">
249 <h2 className="font-display text-4xl">
250 Donate
251 </h2>
252 {donateLinks.map((link) => (
253 <a
254 href={link.href}
255 target="_blank"
256 rel="noopener"
257 className={`link link-${link.iconName}`}
258 title={link.title}
259 >
260 <i
261 className={`icon-2x icon-${link.iconName}`}
262 />
263 </a>
264 ))}
265 </div>
266 </div>
267 </div>
268 <div className="col-med-7">
269 <blockquote className="text-right">
270 Converting coffee to code... <ConsoleSpinner />
271 </blockquote>
272 <p className="text-justify">
273 Hello there,
274 <br />I am Scrumplex, but you can call me Scrum,
275 Scrump or Sefa. I am an enthusiastic developer
276 from <b>Germany</b>, and I invest lots of my
277 time into playing around with many technologies.
278 Linux is my primary interest, as I maintain
279 multiple servers and contribute to various open
280 source technologies around the Linux space. As
281 for programming languages, I primarily use
282 C/C++, Python, Bash, Go, Rust and Kotlin. But I
283 always try to incorporate other languages as
284 well, if they fit the use-case.
285 <br />I know my way around CI/CD professionally,
286 especially working with GitLab CI, Flux CD,
287 Kubernetes and all the technologies around
288 these. And in an effort to apply my DevOps
289 skills at home, I also do lots of{" "}
290 <a
291 href="https://nixos.org"
292 target="_blank"
293 rel="noopener"
294 >
295 <i className="icon-new_tab"></i> Nix
296 </a>{" "}
297 stuff. My daily computing is shaped by{" "}
298 <a
299 href="https://gnu.org/philosophy/free-sw.html"
300 target="_blank"
301 rel="noopener"
302 >
303 <i className="icon-new_tab"></i> free
304 software
305 </a>{" "}
306 and in an effort to ensure that it stays that
307 way I generally publish all my work under a
308 copyleft license. If you want to see what I am
309 working on right now, go to any of the code
310 forges linked on this page.
311 </p>
312 </div>
313 </div>
314 <div className="row">
315 <div className="col text-right">
316 <Footer />
317 </div>
318 </div>
319 </div>
320 <div
321 className={`sheet wavy ${!mainOffset ? "sheet-hidden" : ""}`}
322 id="projects"
323 >
324 <h2 className="font-display text-4xl">Current Projects</h2>
325 <div className="grid grid-cols-2 md:grid-cols-3">
326 <Project
327 title="Prism Launcher"
328 imgPath="prismlauncher.svg"
329 websiteHref="https://prismlauncher.org"
330 sourceHref="https://github.com/PrismLauncher/PrismLauncher"
331 >
332 A custom launcher for Minecraft that allows you to
333 easily manage multiple installations of Minecraft at
334 once.
335 </Project>
336 <Project
337 title="nixpkgs"
338 imgPath="nixos.svg"
339 websiteHref="https://nixos.org"
340 sourceHref="https://github.com/NixOS/nixpkgs"
341 >
342 A reproducible toolchain for package management,
343 system configuration and more.
344 </Project>
345 <Project
346 title="libvibrant"
347 imgPath="vibrant.svg"
348 sourceHref="https://github.com/libvibrant"
349 >
350 A collection of software to adjust color vibrancy
351 and other color correction settings on Linux display
352 servers.
353 </Project>
354 </div>
355 <h2 className="font-display text-4xl">Legacy Projects</h2>
356 <div className="grid grid-cols-2 md:grid-cols-3">
357 <Project
358 title="PASSY"
359 imgPath="passy.svg"
360 sourceHref="https://gitlab.com/PASSYpw/PASSY"
361 >
362 A beautiful password manager utilizing modern web
363 technologies.
364 </Project>
365 <Project
366 title="Waves.js"
367 imgPath="waves.js.svg"
368 sourceHref="https://gitlab.com/PASSYpw/Waves.js"
369 >
370 A JQuery plugin providing authentic Material Design
371 ripples.
372 </Project>
373 <Project
374 title="Sprummlbot"
375 imgPath="sprummlbot.svg"
376 sourceHref="https://gitlab.com/Scrumplex/Sprummlbot"
377 >
378 A lightweight TeamSpeak 3 ServerAdmin Bot, adding
379 many missing features to TeamSpeak 3 servers.
380 </Project>
381 <Project
382 title="ExitNow"
383 imgPath="exitnow.png"
384 sourceHref="https://codeberg.org/Scrumplex/ExitNow"
385 >
386 A simple application, providing an easy method to
387 kill foreground windows with a shortcut.
388 </Project>
389 </div>
390 </div>
391 <div
392 className={`sheet wavy ${!mainOffset ? "sheet-hidden" : ""}`}
393 id="contact"
394 >
395 <h2 className="font-display text-4xl">Contact</h2>
396 <div className="grid grid-cols-1 md:grid-cols-2">
397 <ContactTile
398 title="Email"
399 iconName="mail"
400 href="mailto:contact@scrumplex.net"
401 buttonText="Write an email"
402 >
403 Send me an email at <b>contact@scrumplex.net</b> for
404 questions or help. PGP:{" "}
405 <a
406 href="https://keys.openpgp.org/search?q=contact%40scrumplex.net"
407 target="_blank"
408 rel="noopener"
409 >
410 <code>E13DFD4B47127951</code>
411 </a>
412 </ContactTile>
413 <ContactTile
414 title="Matrix"
415 iconName="matrix"
416 href="https://matrix.to/#/@scrumplex:duckhub.io"
417 buttonText="Message me on Matrix"
418 >
419 Contact me on Matrix via{" "}
420 <b>@scrumplex:duckhub.io</b> and have a conversation
421 with me.
422 </ContactTile>
423 <ContactTile
424 title="Telegram"
425 iconName="telegram"
426 href="https://telegram.me/Scrumplex"
427 buttonText="Message me on Telegram"
428 >
429 Contact me on Telegram at <b>@Scrumplex</b> and have
430 a conversation with me.
431 </ContactTile>
432 </div>
433 </div>
434 <div
435 className={`sheet wavy ${!mainOffset ? "sheet-hidden" : ""}`}
436 id="privacy"
437 >
438 <h2 className="font-display text-4xl">Privacy Policy</h2>
439 <p>
440 Privacy information for services hosted on{" "}
441 <code>scrumplex.net</code>, <code>sefa.cloud</code> and{" "}
442 <code>duckhub.io</code> (and all subdomains)
443 </p>
444 <p>
445 Every service collects log-data. This includes IP
446 addresses, browser user-agents and visited sites. These
447 are not used to track the user, but are only collected
448 for operation purposes.
449 <br />
450 Some services may require you to enter some kind of
451 username or email-address to access them These services
452 can not operate without this information. You may choose
453 to use anonymous or pseudonymous usernames or email
454 addresses. All personal data (such as, but not limited
455 to names, addresses or telephone numbers) shall always
456 be in line with the General Data Protection Regulation.
457 <br />
458 No data is ever processed to track user activity.
459 </p>
460 </div>
461 </div>
462 {!mainOffset && (
463 <div className="scroll-indicator">
464 <h1 className="text-center">▾</h1>
465 </div>
466 )}
467 </>
468 );
469};