Initial commit

main
idylls 1 year ago
commit bae0ce97ad
Signed by: idylls
GPG Key ID: 8A7167CBC2CC9F0F

3
.gitignore vendored

@ -0,0 +1,3 @@
/target
node_modules/
build/

@ -0,0 +1,7 @@
#!/bin/sh
cp -r content/static build/
npx lessc css/style.less build/style.css
npx lessc css/resume.less build/resume.css
npx esbuild --bundle js/bundle.ts --outfile=build/bundle.js
bun gen/main.ts

@ -0,0 +1,41 @@
<x-meta>
<title>About the Author</title>
</x-meta>
<h1>About the Author</h1>
<div>
<img class="author" src="static/images/author.png" />
<p>
Hello! My name is Nick. Currently I'm located in the Greater Philadelphia
area.
</p>
<p>
I <a href="/work.html">work</a> as a programmer, typically remotely.
I'm usually tinkering on some sort of project, some of which are available
publicly on <a href="https://git.idylls.net">my git service</a>, and others
that I keep mostly private for myself and friends (though I'm typically
happy to demo/share source code on request).
</p>
<p class="hidden">
Outside of that, I've recently
started doing some art again, which I publish under the aliases Opus Manuum
(for more traditional art) and 0xFA7E (for pixel art). Eventually there
will be a link here where you can view some of my pieces :)
</p>
<p>
You can see what I'm currently up to on my <a href="/now.html">now</a> page.
</p>
<p>
I maintain a list of the gear / tools I use <a href="/uses.html">here</a>.
</p>
<h2 id="contact">Contact Me</h2>
<p>
If you'd like to reach me, the best way to do so is by email. My email
address is <code class="nobreak">nick at &lt;this domain&gt;</code>. <strong>Please add my
address to your whitelist, otherwise any responses I send may be sent to
spam, or worse, silently dropped.</strong> I try to respond within 24 hours,
so if you haven't heard from me by then, either check your spam folder or
send me another email.
</p>
</div>

@ -0,0 +1,3 @@
<x-meta>
<title>Home</title>
</x-meta>

@ -0,0 +1,5 @@
<x-meta>
<title>Hello</title>
</x-meta>
<x-home-quote />

@ -0,0 +1,25 @@
<x-meta>
<title>Logs</title>
</x-meta>
<h1>Logs</h1>
<p>
I have a terrible memory, so I try to keep track of various media that I've
enjoyed here. Think of it kind of like Goodreads :)
</p>
<p>
Everything is rated out of 10, with the general scale as follows:
<div>10: All time favorite</div>
<div>9: Almost perfect</div>
<div>8: Really good</div>
<div>7: Good</div>
<div>6: Decent</div>
<div>5: Meh</div>
<div>4: Not my thing</div>
<div>3: Really not my thing</div>
<div>2: Bad, but maybe someone out there would like it</div>
<div>1: Awful</div>
</p>
<x-logs />

@ -0,0 +1,13 @@
<x-meta>
<title>Sitemap</title>
</x-meta>
<h1>Site map</h1>
<ul>
<li><a href="/index.html">Home</a></li>
<li><a href="/about.html">About</a></li>
<li><a href="/work.html">Work</a></li>
<li><a href="/logs.html">Logs</a></li>
<li><a href="/now.html">Now</a></li>
</ul>

@ -0,0 +1,7 @@
<x-meta>
<title>Now</title>
</x-meta>
<h1>Now</h1>
<x-now />

@ -0,0 +1,80 @@
<x-meta>
<title>Uses This</title>
</x-meta>
<h1>Uses This</h1>
<p>Inspired by <a href="https://usesthis.com">usesthis</a>.</p>
<section>
<h2>Computing</h2>
<section>
<h3>Desktop</h3>
<p>Built February 2022</p>
<ul>
<li>Case: Corsair 4000D Airflow</li>
<li>Motherboard: Asus PRIME Z690-P D4</li>
<li>Processor: i7-12700k</li>
<li>GPU: EVGA GeForce RTX 3060 Ti FTW Ultra Gaming</li>
<li>RAM: 32GB DDR4 @ 3600MHz</li>
<li>Cooler: Scythe FUMA 2</li>
<li>Power Supply: Corsair RM750x (2021)</li>
<li>Drive: Samsung 970 Evo Plus 1TB</li>
<li>Operating System: Arch Linux</li>
<li>Mouse: Pulsar Xlite V2 mini Wireless</li>
<li>Headphones: Sennheiser HD 560 S</li>
<li>Microphone: Samson Technologies Q2U Dynamic Microphone</li>
<li>Mouse pad: ARTISAN Raiden XL</li>
</ul>
</section>
<section>
<h3>Laptop</h3>
<p>
Lenovo Thinkpad T440s, bought sometime in ~2013. Running Arch Linux.
Modded with a 3-button touchpad. Looking to replace this at some point
in the near future.
</p>
</section>
<section>
<h3>Keyboard</h3>
<p>
Self-built
<a href="https://github.com/davidphilipbarr/Sweep">Ferris Sweep</a>, with
<a href="https://boardsource.xyz/store/5fff705f03db380da20f1014">Choc Purpz</a>.
<p>
</section>
<section>
<h3>Tools</h3>
<ul>
<li>Text editor: Emacs</li>
<li>Terminal emulator: kitty</li>
<li>Interactive shell: fish</li>
<li>Browser: Firefox</li>
</ul>
</section>
</section>
<section>
<h2>Art</h2>
<ul>
<li>Digital art software: Krita</li>
<li>Tablet: Wacom Intuous S</li>
<li>Sketchbook: Some cheap hardcover notebooks from Amazon</li>
<li>Pens: Mr. Pen Multiliners</li>
<li>Mechanical Pencil: uni Kuru Toga Roulette</li>
</ul>
</section>
<section>
<h2>Other</h2>
</section>
<section>
<h2>Updates</h2>
<ul>
<li>2023-01-12: Created this page</li>
</ul>
</section>
<section>
<h2>Archive</h2>
</section>

@ -0,0 +1,15 @@
<x-meta>
<title>Work</title>
</x-meta>
<h1>Work</h1>
<p>
I am currently looking for work! If you'd like to contact me, see
<a href="/about.html#contact">here</a>.
</p>
<p>
My full résumé is available <a href="/resume.html"> here</a>.
</p>
<x-work />

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

@ -0,0 +1,165 @@
/* Reset */
*, *::before, *::after {
box-sizing: border-box;
padding: 0;
margin: 0;
}
:root {
min-width: 100vw;
min-height: 100vh;
}
body {
padding: 0;
margin: 0;
}
/* Style */
:root {
font-family: "Inter";
font-size: 18pt;
}
body {
padding: 1.5rem;
line-height: 1.3;
max-width: 100ch;
margin: 0 auto;
}
a {
color: inherit;
}
.info {
display: grid;
grid-template-columns: 1fr min-content;
.name {
grid-row: 1 / 3;
font-size: 2rem;
}
margin-bottom: 0.5em;
}
section {
header {
font-size: 1.5rem;
margin-bottom: 0.15rem;
}
margin-bottom: 0.5em;
}
.experience {
display: grid;
grid-template-columns: max-content 1fr max-content;
margin-bottom: 0.5em;
.title {
font-weight: 600;
}
.company {
font-style: italic;
&::before {
font-style: normal;
display: inline-block;
content: "\00a0|\00a0";
}
}
.skills {
grid-column: 1 / 4;
}
.highlights {
grid-column: 1 / 4;
font-size: 0.9rem;
padding-left: 1em;
}
}
.projects {
display: grid;
grid-template-columns: 1fr 1fr;
aside {
display: flex;
align-items: center;
justify-content: flex-end;
font-style: italic;
text-align: right;
font-size: 0.8rem;
}
.project {
display: grid;
grid-template-columns: max-content 1fr;
grid-template-rows: repeat(2, min-content);
.name {
font-weight: 600;
}
.skills {
&::before {
font-style: normal;
display: inline-block;
content: "\00a0|\00a0";
}
}
.description {
grid-column: 1 / 3;
}
}
}
.education {
display: grid;
grid-template-columns: fit-content(100%) max-content;
header {
grid-column: 1 / 3;
}
.university {
font-weight: 600;
&::after {
font-weight: normal;
display: inline-block;
content: "\00a0\2014\00a0"
}
}
.degree {
grid-column: 1 / 3;
}
}
@media print {
body {
padding: 0;
margin: 1.5rem;
}
@page {
margin: 0;
}
:root {
font-size: 14pt;
}
section.projects {
padding-top: 22px;
}
}

@ -0,0 +1,171 @@
/* Reset */
*, *::before, *::after {
box-sizing: border-box;
}
:root {
min-width: 100vw;
min-height: 100vh;
}
body {
padding: 0;
margin: 0;
}
ul, ol {
margin: 0;
}
blockquote {
margin: 0;
}
h1, h2, h3, h4, h5, h6 {
font-size: unset;
margin: 0;
font-style: unset;
font-weight: unset;
}
p {
margin: 0;
line-height: unset;
}
/* Site Elements */
:root {
--background: black;
--foreground: white;
--base-font-size: 18pt;
background: var(--background);
color: var(--foreground);
font-size: 18pt;
font-family: sans;
}
a {
position: relative;
color: inherit;
text-decoration: underline;
&[href^="http"]::after {
display: inline-block;
content: "🡕";
margin-left: 3px;
font-weight: bold;
font-size: 0.85em;
}
&:hover {
text-decoration-thickness: 3px;
}
}
nav {
padding: 1em;
ul {
padding: 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
li {
list-style: none;
margin-right: 1em;
}
}
}
main {
margin: 1rem auto;
padding: 0 1em;
max-width: 75ch;
display: flex;
flex-direction: column;
}
.home-quote {
font-size: 1.25rem;
margin: 1.5em;
cite {
font-style: normal;
font-size: 1.1rem;
margin-top: 1em;
display: block;
&::before {
display: inline-block;
content: "\2014\00a0";
}
}
}
img.author {
float: left;
max-width: 256px;
margin-right: 2em;
margin-bottom: 2em;
}
p {
margin-bottom: 1em;
}
h1 {
font-size: 2.2rem;
margin-bottom: 0.5em;
margin-top: 0.5em;
}
h2 {
font-size: 1.8rem;
margin-bottom: 0.5em;
margin-top: 0.5em;
}
h3 {
font-size: 1.4rem;
margin-bottom: 0.5em;
margin-top: 0.5em;
}
h4 {
font-size: 1.2rem;
margin-bottom: 0.5em;
margin-top: 0.5em;
}
.now {
section {
margin-bottom: 1em;
}
.date {
font-weight: bold;
}
}
.nobreak {
display: inline-block;
}
.full-bleed {
width: 100vw;
margin-left: calc(50% - 50vw);
}
table {
border-spacing: 25px 10px;
}
table.work {
max-width: 85vw;
margin: 0 auto;
}
.hidden {
display: none;
}

@ -0,0 +1,5 @@
#!/bin/sh
./build.sh
rsync -avz -e 'ssh -p 8022' build/* root@idylls.net:/var/www/htdocs/idylls.net/

@ -0,0 +1,18 @@
import { buildPages } from "./pages/build";
const abort = (e: any) => {
console.error(e);
process.exit(1);
};
const check = <T>(p: Promise<T>) => p.catch(abort);
const gen = async () => {
const bp = check(buildPages());
await bp;
console.log("Generation done");
};
await gen();

@ -0,0 +1,131 @@
import { readdir, mkdir, stat, readFile, writeFile } from "node:fs/promises";
import { HTMLElement, parse, NodeType } from "node-html-parser";
import { render as renderNow } from "./now";
import { render as renderHomeQuote } from "./quote";
import { render as renderLogs } from "./logs";
import { render as renderWork, renderResume } from "./work";
const walkdir = async (path: string) => {
const paths = await readdir(path);
const out = [] as string[];
for (const p of paths) {
const p_ = `${path}/${p}`;
const st = await stat(p_);
if (st.isDirectory()) {
// TODO: do something special
continue;
}
out.push(p_);
}
return out;
};
const RENDERERS = {
"x-meta": () => "",
"x-now": renderNow,
"x-logs": renderLogs,
"x-home-quote": renderHomeQuote,
"x-work": renderWork,
};
const renderElement = (el: HTMLElement) => {
if (el.nodeType == NodeType.TEXT_NODE) {
return el.rawText;
}
const renderer = RENDERERS[el.tagName.toLowerCase()] ?? renderBasic;
return renderer(el);
};
const renderBasic = (el: HTMLElement) => {
const tag = el.tagName.toLowerCase();
let out = `<${tag}`;
for (const attr in el.attributes) {
out += ` ${attr}="${el.attributes[attr]}"`;
}
out += ">";
const children = el.childNodes.map(renderElement).join("");
out += children;
out += `</${tag}>`;
return out;
};
const buildPage = async (pagePath: string) => {
const contents = (await readFile(pagePath)).toString();
const parsed = parse(contents);
const pageOpts = (() => {
const el = parsed.querySelector("x-meta");
if (!el) {
return {
title: null,
};
}
const title = el.querySelector("title");
return {
title: title?.innerText ?? null,
};
})();
const els = parsed.childNodes.map(renderElement).join("");
const rendered = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="utf-8" />
<link rel="stylesheet" href="/style.css" />
<script src="/bundle.js"></script>
${
pageOpts.title
? `<title>${pageOpts.title} - idylls.net</title>`
: "<title>idylls.net</title>"
}
</head>
<body>
<nav>
<ul>
<li><a href="/index.html">Home</a></li>
<li><a href="/about.html">About</a></li>
<li><a href="/about.html#contact">Contact</a></li>
<li><a href="/work.html">Work</a></li>
<li><a href="https://git.idylls.net">Git</a></li>
<li><a href="https://status.idylls.net">Services</a></li>
<li><a href="/nav.html">More</a></li>
</ul>
</nav>
<main>
${els}
</main>
</body>
</html>
`;
const [, p] = pagePath.split("content/pages/");
const outPath = `build/${p}`;
return writeFile(outPath, rendered);
};
export const buildPages = async () => {
await mkdir("build/", { recursive: true });
const paths = await walkdir("content/pages");
for (const p of paths) {
await buildPage(p);
}
const resumeContent = renderResume();
await writeFile("build/resume.html", resumeContent);
};

@ -0,0 +1,165 @@
import { isoDateStr } from "../util";
type LogBase = {
name: string;
started?: Date;
finished?: Date;
dropped?: true;
rating?: number;
picUrl?: string;
};
const status = (log: LogBase) => {
if (log.dropped) {
return "dropped" as const;
}
if (!log.started && !log.finished && !log.rating) {
return "wantTo" as const;
}
if (log.started && !log.finished) {
return "inProgress" as const;
}
if (log.finished || log.rating) {
return "finished" as const;
}
throw new Error("unreachable");
};
const byStatusSorted = (items: LogBase[]) => {
const inProgress = items.filter((i) => status(i) == "inProgress");
inProgress.sort((a, b) => +(a.started ?? 0) - +(b.started ?? 0));
const finished = items.filter((i) => status(i) == "finished");
finished.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0));
const wantTo = items.filter((i) => status(i) == "wantTo");
wantTo.sort((a, b) => a.name.localeCompare(b.name));
const dropped = items.filter((i) => status(i) == "dropped");
dropped.sort((a, b) => +((a.started ?? 0) > (b.started ?? 0)));
return { dropped, wantTo, finished, inProgress };
};
type Anime = LogBase & { season?: number };
const ANIME: Anime[] = [
{
name: "Violet Evergarden",
rating: 10,
},
{
name: "Sugar Apple Fairy Tale",
started: new Date("2023-01-22"),
},
{
name: "REVENGER",
started: new Date("2023-01-21"),
},
{
name: "Ice Guy and the Cool Female Colleague",
},
{
name: "High Card",
started: new Date("2023-01-21"),
},
{
name: "Bocchi the Rock!",
started: new Date("2023-01-12"),
season: 1,
},
{
name: "Blue Period",
started: new Date("2023-01-02"),
finished: new Date("2023-01-12"),
season: 1,
rating: 10,
},
{
name: "Chainsaw Man",
started: new Date("2022-12-01"),
finished: new Date("2023-01-04"),
season: 1,
rating: 7.5,
},
{
name: "Do it Yourself!!",
started: new Date("2022-12-01"),
season: 1,
},
{
name: "Cyberpunk: Edgerunners",
season: 1,
rating: 10,
},
{
name: "The Bear",
rating: 8,
},
{
name: "The Witcher",
rating: 9,
},
{
name: "Arcane",
rating: 10,
},
{
name: "Castlevania",
rating: 10,
},
{
name: "Over the Garden Wall",
rating: 10,
},
];
const renderAnime = () => {
const renderSection = (title: string, items: Anime[]) => {
let out = `<section><h3>${title}</h3>`;
for (const a of items) {
out += `<div class="log anime"><h4>${a.name}`;
if (a.season) {
out += `, Season ${a.season}`;
}
out += "</h4>";
if (a.started) {
out += `<div class="started">Started: ${isoDateStr(a.started)}</div>`;
}
if (a.finished) {
out += `<div class="finished">Finished: ${isoDateStr(
a.finished,
)}</div>`;
}
if (a.rating) {
out += `<div class="rating">Rating: ${a.rating} / 10</div>`;
}
out += "</div>";
}
out += "</section>";
return out;
};
const { wantTo, dropped, finished, inProgress } = byStatusSorted(ANIME);
return `
<section>
<h2>Anime / TV</h2>
${renderSection("Currently Watching", inProgress)}
${renderSection("Finished", finished)}
${renderSection("Dropped", dropped)}
${renderSection("Want to Watch", wantTo)}
</section>
`;
};
export const render = () => {
return renderAnime();
};

@ -0,0 +1,95 @@
import { isoDateStr, dbg } from "../util";
type EntryKind =
| {
kind: "free";
text: string;
}
| {
kind: "listeningTo";
artist: string;
song: string;
url?: string;
}
| {
kind: "workingOn";
what: string;
url?: string;
};
type Entry = {
date: Date;
entries: EntryKind[];
};
const ENTRIES: Entry[] = [
{
date: new Date("2023-01-10"),
entries: [{ kind: "workingOn", what: "this site!" }],
},
{
date: new Date("2023-01-18"),
entries: [
{ kind: "free", text: "Looking for work!" },
{
kind: "listeningTo",
artist: "Saint Pepsi",
song: "Better",
url: "https://music.youtube.com/watch?v=WYvji5AXOfk",
},
],
},
{
date: new Date("2023-01-12"),
entries: [
{
kind: "listeningTo",
artist: "Darius (feat. Amaria)",
song: "FADED",
url: "https://music.youtube.com/watch?v=SfTOJ9PeQlY",
},
],
},
];
export const render = () => {
const entries = [...ENTRIES];
entries.sort((a, b) => +b.date - +a.date);
let out = '<div class="now">';
for (const entry of entries) {
out += `
<section>
<div class="date">
${isoDateStr(entry.date)}
</div>`;
for (const en of entry.entries) {
switch (en.kind) {
case "workingOn":
out += `<div class="working-on">Working on: ${en.what}</div>`;
break;
case "free":
out += `<div class="free">${en.text}</div>`;
break;
case "listeningTo":
out += `
<div class="listening-to">
Listening to:
<a href="${en.url}">
<span class="artist">${en.artist}</span>
-
<span class="song">${en.song}</span>
</a>
</div>
`;
break;
}
}
out += `</section>`;
}
out += "</div>";
return out;
};

@ -0,0 +1,18 @@
// TODO: now that this is in typescript, maybe share quote list and
// dynamically pick one here?
export const render = () => `
<div class="home-quote">
<blockquote>
The world ain't all sunshine and rainbows. It's a very mean and
nasty place, and I don't care how tough you are it will beat you
to your knees and keep you there permanently if you let it. You,
me, or nobody is gonna hit as hard as life. But it ain't about
how hard you hit. It's about how hard you can get hit and keep
moving forward. How much you can take and keep moving forward.
That's how winning is done!
<cite>Rocky</cite>
</blockquote>
<script>initQuote()</script>
</div>
`;

@ -0,0 +1,327 @@
import { isoDateStr } from "../util";
type Info = {
name: string;
emailAddress: string;
website: string;
};
type Education = {
university: string;
college: string;
graduationYear: number;
degree: string;
};
type Timeframe = { start: Date; end?: Date } | string;
type Skill =
| "TypeScript"
| "React"
| "Python"
| "Django"
| "CSS"
| "SCSS"
| "PostgreSQL"
| "Java"
| "Go"
| "MongoDB"
| "WordPress"
| "PHP"
| "Kotlin"
| "HTML"
| "Elixir"
| "Phoenix"
| "JavaScript"
| "Docker"
| "Rust"
| "C++"
| "C"
| "Haskell"
| "GraphQL";
type Experience = {
companyName: string;
title: string;
skills: Skill[];
timeframe: Timeframe;
highlights: string[];
};
type Project = {
name: string;
skills: Skill[];
description: string;
link?: string;
};
type Resume = {
info: Info;
education: Education;
experiences: Experience[];
selectedProjects: Project[];
skills: Skill[];
};
const RESUME: Resume = {
info: {
name: "Nick",
emailAddress: "hello@idylls.net",
website: "https://idylls.net",
},
education: {
university: "Northeastern University",
college: "Khoury College of Computer Sciences",
degree: "Bachelor of Science in Computer Science with Honors, Cum Laude",
graduationYear: 2019,
},
experiences: [
{
companyName: "Massachusetts Bay Transportation Authority",
title: "Full Stack Software Engineer",
highlights: [
"Led maintenance and development of critical, high-availability service delivering transit alerts to riders via both SMS and email",
"Developed and deployed various improvements to public-facing rider website",
],
skills: [
"Elixir",
"Phoenix",
"TypeScript",
"JavaScript",
"SCSS",
"Docker",
"PostgreSQL",
],
timeframe: "2022",
},
{
companyName: "KickUp",
title: "Full Stack Software Engineer",
highlights: [
"Designed and implemented several features improving client abilities to visualize, analyze, and action teacher growth data",
"Led effort to integrate TypeScript into existing JavaScript codebase, as well as provided mentorship to other team members",
],
skills: [
"TypeScript",
"JavaScript",
"React",
"Python",
"Django",
"CSS",
"PostgreSQL",
"GraphQL",
],
timeframe: {
start: new Date("2020-10-01"),
end: new Date("2022-02-01"),
},
},
{
companyName: "TripAdvisor",
title: "Full Stack Software Engineer",
highlights: [
"Led design and implementation of project surfacing location outliers to suppliers, increasing sales and click-through rate on 10% of products",
"Implemented improved online photo editor, increasing the percentage of listings with owner-provided photos by 5%",
],
timeframe: {
start: new Date("2019-09-01"),
end: new Date("2020-06-01"),
},
skills: ["Java", "JavaScript", "React", "CSS", "PostgreSQL", "GraphQL"],
},
{
companyName: "TripAdvisor, Rentals",
title: "Full Stack Engineering Co-op",
highlights: [
"Assisted in redesigning the external calendar syncing system, reducing the time to update hundreds of thousands of external calendars from hours to fifteen minutes",
"Designed, developed, and tested endpoints for internal and external REST APIs in a large Java codebase, as well as modified user-facing web pages",
],
timeframe: "Spring/Summer 2018",
skills: ["Java", "JavaScript", "CSS"],
},
{
companyName: "clypd",
title: "Backend Engineering Co-op",
highlights: [
"Acted as feature lead on redesign of large portions of existing API to surface additional data and improve ergonomics for major analytics project",
"Organized meetups originally related to Rust but later expanding to general code jams",
],
skills: ["Go", "PostgreSQL"],
timeframe: "Spring/Summer 2017",
},
{
companyName: "I'm From the Future",
title: "Junior Full Stack Software Engineer",
highlights: [
"Led development on an internal marketing tool using React, D3.js, PhantomJS, and MongoDB",
],
skills: ["PHP", "JavaScript", "React", "MongoDB", "CSS", "WordPress"],
timeframe: "Summer/Fall 2016",
},
{
companyName: "ProTech Internet Group",
title: "Junior Web Developer",
skills: ["PHP", "JavaScript", "CSS", "WordPress"],
timeframe: {
start: new Date("2015-06-01"),
end: new Date("2016-06-01"),
},
highlights: [],
},
{
companyName: "SiteTechs",
title: "Web Content Admin",
skills: ["JavaScript", "HTML", "CSS", "WordPress"],
timeframe: {
start: new Date("2012-06-01"),
end: new Date("2015-09-01"),
},
highlights: [],
},
],
selectedProjects: [
{
name: "Pathos",
description:
"An Old School RuneScape plugin for displaying pathing information",
skills: ["Kotlin"],
link: "https://git.idylls.net/idylls/pathos",
},
{
name: "Statue",
description: "A lightweight web host / service status monitor",
skills: ["Rust", "TypeScript"],
link: "https://status.idylls.net",
},
],
skills: ["Rust", "TypeScript", "C++", "C", "Haskell", "Java"],
};
export const renderResume = () => {
const renderInfo = () => `
<section class="info">
<div class="name">${RESUME.info.name}</div>
<div class="email">${RESUME.info.emailAddress}</div>
<div class="website">
<a href="${RESUME.info.website}">
${RESUME.info.website}
</a>
</div>
</section>
`;
const renderExperiences = () => {
let out = `<section class="experiences"><header>Experience</header>`;
for (const e of RESUME.experiences) {
out += `<div class="experience">`;
out += `<div class="title">${e.title}</div>`;
out += `<div class="company">${e.companyName}</div>`;
out += `<div class="timeframe">${renderTimeframe(e.timeframe)}</div>`;
out += `<div class="skills">${e.skills.join(", ")}</div>`;
out += `<ul class="highlights">`;
for (const h of e.highlights) {
out += `<li>${h}</li>`;
}
out += `</ul>`;
out += `</div>`;
}
out += `</section>`;
return out;
};
const renderProjects = () => {
let out = `<section class="projects"><header>Selected Projects</header>`;
out += `<aside>See more at&nbsp;<a href="https://git.idylls.net/idylls">https://git.idylls.net/idylls</a></aside>`;
for (const p of RESUME.selectedProjects) {
out += `<div class="project">`;
out += `<div class="name">${p.name}</div>`;
out += `<div class="skills">${p.skills.join(", ")}</div>`;
out += `<div class="description">${p.description}</div>`;
out += `</div>`;
}
out += "</section>";
return out;
};
const renderEducation = () => `
<section class="education">
<header>Education</header>
<div class="university">${RESUME.education.university}</div>
<div class="college">${RESUME.education.college} Class of ${RESUME.education.graduationYear}</div>
<div class="degree">${RESUME.education.degree}</div>
</section>
`;
const renderSkills = () => `
<section class="skills">
<header>Skills</header>
<div>${RESUME.skills.join(", ")}</div>
</section>
`;
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="utf-8" />
<link rel="stylesheet" href="/resume.css" />
<title>Résumé</title>
</head>
<body>
${renderInfo()}
${renderExperiences()}
${renderProjects()}
${renderEducation()}
${renderSkills()}
</body>
</html>
`;
};
const renderTimeframe = (t: Timeframe) => {
if (typeof t == "string") {
return t;
}
return `${t.start.getFullYear()}${
t.end ? t.end.getFullYear() : "Present"
}`;
};
export const render = () => {
let out = `
<div class="full-bleed">
<table class="full-bleed work">
<thead>
<tr>
<th>Company</th>
<th>Title</th>
<th>Timeframe</th>
</tr>
</thead>
<tbody>
`;
for (const e of RESUME.experiences) {
out += `
<tr>
<td>${e.companyName}</td>
<td>${e.title}</td>
<td>${renderTimeframe(e.timeframe)}</td>
</tr>
`;
}
out += `</tbody></table></div>`;
return out;
};

@ -0,0 +1,11 @@
export const isoDateStr = (date: Date) =>
`${date.getUTCFullYear()}-${`${date.getUTCMonth() + 1}`.padStart(
2,
"0",
)}-${`${date.getUTCDate()}`.padStart(2, "0")}`;
export const dbg = <T>(a: T) => {
console.debug(a);
return a;
};

@ -0,0 +1,3 @@
import { init as initQuote } from "./quote";
window["initQuote"] = initQuote;

@ -0,0 +1,55 @@
import { swel } from "swel";
const QUOTES = [
[
"The world ain't all sunshine and rainbows. It's a very mean and nasty place, and I don't care how tough you are it will beat you to your knees and keep you there permanently if you let it. You, me, or nobody is gonna hit as hard as life. But it ain't about how hard you hit. It's about how hard you can get hit and keep moving forward. How much you can take and keep moving forward. That's how winning is done!",
"Rocky",
] as const,
[
"Almost dead yesterday, maybe dead tomorrow, but alive, gloriously alive, today",
"Robert Jordan, <em>Lord of Chaos<em>",
] as const,
[
"Those who build walls are their own prisoners",
"Ursula K. Le Guin, <em>The Dispossessed</em>",
] as const,
[
"I have no right to call myself one who knows. I was one who \
seeks, and I still am, but I no longer seek in the stars or in \
the books; Im beginning to hear the teachings of my blood \
pulsing within me. My story isnt pleasant. Its not sweet and \
harmonious like invented stories. It tastes of folly and \
bewilderment, of madness and dream, like the life of all people \
who no longer want to lie to themselves.",
"Herman Hesse",
] as const,
[
"In the depth of winter, I finally learned that within me there \
lay and invincible summer",
"Albert Camus",
] as const,
[
"Life, although it may only be an accumulation of anguish, is \
dear to me, and I will defend it.",
"Mary Shelley, <em>Frankenstein</em>",
] as const,
[
"Who but a god can go through life unmarked?",
"Aeschylus, <em>Agamemnon</em>",
] as const,
["Don't you dare go Hollow.", "Laurentius, <em>Dark Souls</em>"] as const,
];
export const init = () => {
const el = document.querySelector(".home-quote");
if (!el) {
return;
}
const [quote, cite] = QUOTES[Math.floor(Math.random() * QUOTES.length)];
const citeEl = swel("cite");
citeEl.innerHTML = cite;
const bq = swel("blockquote", [quote, citeEl]);
el.replaceChildren(bq);
};

1229
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,17 @@
{
"name": "sike",
"type": "module",
"version": "0.0.0",
"description": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"devDependencies": {
"bun-types": "^0.5.0",
"esbuild": "^0.16.17",
"less": "^4.1.3",
"node-html-parser": "^6.1.4",
"swel": "file:../swel"
}
}

@ -0,0 +1,11 @@
module.exports = {
printWidth: 80,
useTabs: true,
semi: true,
singleQuote: false,
quoteProps: "as-needed",
trailingComma: "all",
bracketSpacing: true,
arrowParens: "always",
parser: "typescript",
};

@ -0,0 +1,17 @@
{
"compilerOptions": {
"noEmit": true,
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"noUncheckedIndexedAccess": true,
"strictNullChecks": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"checkJs": true,
"allowJs": true,
"types": ["bun-types"]
},
"include": ["gen/**/*.ts", "gen/**/*.js"],
"exclude": ["node_modules"]
}
Loading…
Cancel
Save