My personal website
at refactor/vite 469 lines 20 kB view raw
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};