Compare commits

..

14 Commits

Author SHA1 Message Date
Avraham Sakal 9909ee6d94 feat: responsive headshot 2026-05-24 11:39:35 -04:00
Avraham Sakal 91931039bc more mobile-friendly 2026-05-24 10:18:24 -04:00
Avraham Sakal f983fe5b83 include latest pdf in deployment bundle, not previous pdf 2025-09-30 09:34:47 -04:00
Avraham Sakal acb5d225c4 ensure wrangler doesn't ignore pdf 2025-09-30 09:17:41 -04:00
Avraham Sakal 774c7766d6 generate downloadable pdf on deploy 2025-09-30 09:15:07 -04:00
Avraham Sakal 30fb5d57ce subtle bounce effect on print button 2025-09-29 08:26:29 -04:00
Avraham Sakal 9ef949550e update wrangler 2025-09-28 23:00:22 -04:00
Avraham Sakal 46f416c6b5 add "print" button 2025-09-28 22:56:39 -04:00
Avraham Sakal c3d539ffbd add this resume to projects/hobbies 2025-09-28 22:28:02 -04:00
Avraham Sakal a701b6a855 feat: projects/hobbies: link to deployment and code 2025-09-28 22:24:36 -04:00
Avraham Sakal 51c8effaf2 import image to include in build assets 2025-08-25 08:06:23 -04:00
Avraham Sakal a5999f1606 remove import of no-longer-existing css 2025-08-25 07:58:15 -04:00
Avraham Sakal 7aed841226 rename cloudflare project 2025-08-24 15:18:06 -04:00
Avraham Sakal 3b47596a57 major: statically-generated resume with Vike 2025-08-24 15:13:02 -04:00
24 changed files with 6518 additions and 396 deletions
+142
View File
@@ -0,0 +1,142 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Cloudflare
.wrangler/
# Vercel
.vercel/
# Sentry Vite Plugin
.env.sentry-build-plugin
# aws-cdk
.cdk.staging
cdk.out
+2
View File
@@ -0,0 +1,2 @@
nodejs 22.15.0
pnpm 9.7.1
+53
View File
@@ -0,0 +1,53 @@
Generated with [vike.dev/new](https://vike.dev/new) ([version 471](https://www.npmjs.com/package/create-vike/v/0.0.471)) using this command:
```sh
pnpm create vike@latest --react --compiled-css --cloudflare --eslint
```
## Contents
* [React](#react)
* [`/pages/+config.ts`](#pagesconfigts)
* [Routing](#routing)
* [`/pages/_error/+Page.jsx`](#pages_errorpagejsx)
* [`/pages/+onPageTransitionStart.ts` and `/pages/+onPageTransitionEnd.ts`](#pagesonpagetransitionstartts-and-pagesonpagetransitionendts)
* [SSR](#ssr)
* [HTML Streaming](#html-streaming)
## React
This app is ready to start. It's powered by [Vike](https://vike.dev) and [React](https://react.dev/learn).
### `/pages/+config.ts`
Such `+` files are [the interface](https://vike.dev/config) between Vike and your code. It defines:
* A default [`<Layout>` component](https://vike.dev/Layout) (that wraps your [`<Page>` components](https://vike.dev/Page)).
* A default [`title`](https://vike.dev/title).
* Global [`<head>` tags](https://vike.dev/head-tags).
### Routing
[Vike's built-in router](https://vike.dev/routing) lets you choose between:
* [Filesystem Routing](https://vike.dev/filesystem-routing) (the URL of a page is determined based on where its `+Page.jsx` file is located on the filesystem)
* [Route Strings](https://vike.dev/route-string)
* [Route Functions](https://vike.dev/route-function)
### `/pages/_error/+Page.jsx`
The [error page](https://vike.dev/error-page) which is rendered when errors occur.
### `/pages/+onPageTransitionStart.ts` and `/pages/+onPageTransitionEnd.ts`
The [`onPageTransitionStart()` hook](https://vike.dev/onPageTransitionStart), together with [`onPageTransitionEnd()`](https://vike.dev/onPageTransitionEnd), enables you to implement page transition animations.
### SSR
SSR is enabled by default. You can [disable it](https://vike.dev/ssr) for all your pages or only for some pages.
### HTML Streaming
You can enable/disable [HTML streaming](https://vike.dev/stream) for all your pages, or only for some pages while still using it for others.
+1
View File
@@ -0,0 +1 @@
*.pdf
Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

-44
View File
@@ -1,44 +0,0 @@
@import url("https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400..800;1,400..800&display=swap");
/* @font-face {
font-family: Lato;
src: url("Lato2OFL/Lato-Regular.ttf");
}
@font-face {
font-family: Lato;
src: url("Lato2OFL/Lato-Bold.ttf");
font-weight: bold;
}
@font-face {
font-family: Lato;
src: url("Lato2OFL/Lato-Italic.ttf");
font-style: italic;
}
@font-face {
font-family: Lato;
src: url("Lato2OFL/Lato-BoldItalic.ttf");
font-style: italic;
font-weight: bold;
} */
/* @page {
@bottom-center {
content: "https://sakal.us";
font-size: 0.8em;
color: #aaa;
font-family: Lato;
}
@top-center {
content: "CSS Paged Media Tutorial by Andreas Jung";
font-size: 0.8em;
color: #aaa;
font-family: Lato;
}
} */
/* body {
font-family: Lato;
} */
+193
View File
@@ -0,0 +1,193 @@
export const contactInfo = {
name: "Brian Sakal",
email: "brian@sakal.us",
phone: "305-930-0248",
website: "https://git.sakal.us/avraham",
location: "Passaic, NJ",
};
export const mainSkills = [
"Javascript/Typescript",
"React",
"Next.js",
"Node.js",
"Express.js",
"Docker",
"MySQL",
"Clickhouse",
"DevOps",
"Kubernetes",
"Linux",
"Cloudflare",
"Vercel",
"S3",
];
export const exposedSkills = [
"PostgreSQL",
"Elasticsearch",
"Redis",
"RethinkDB",
"Helm",
"nginx",
"React Native",
"Swagger",
];
export const employment = [
{
title: "Lead Developer (Full-Stack)",
company: "TorahAnytime.com",
location: "Flushing, NY",
start: "June 2022",
end: "Present",
highlights: [
// From old resume:
// "Fixed/troubleshooted bugs and added features across multiple legacy codebases",
// "Consolidated cloud resources by using Kubernetes.",
// "Transitioned a legacy Cloudflare/nginx TLD site to Vercel, while maintaining custom nginx routes on the TLD, plus the entire legacy site under a subdomain.",
// "Reduced time-to-deployment from 5 minutes to 15 seconds with locally-runnable deploy scripts; besides setting up Drone/Gitlab CI/CD pipelines.",
// "Architected custom analytics backend w/ Clickhouse. Reduced query times from minutes to milliseconds.",
// "Implemented a custom IVR phone system to dynamically browse content.",
// "Deployed and configured various supporting/ancillary services in Kubernetes, including Gitlab, private Docker & NPM registries.",
// "Implemented OpenTelemetry tracing visualized in Grafana, decreasing transcoder troubleshooting by 50%.",
// "Implemented various asynchronous workflows, including a new video transcoder, in Temporal; thus completely severing our dependence on Vimeo, reducing costs by $40k/year.",
// From Otta resume:
"General full-stack work (frontend implementation in Next.js, adding endpoints to api, running database migrations in MySQL, updating Clickhouse with those schema changes, etc.)",
"Architected and implemented an in-house transcoding/content-delivery system based on Temporal, Cloudflare, and Backblaze B2 to replace Vimeo; saving $70k/year.",
"Setup generic events analytics system in Clickhouse to replace MySQL, Redis, and (eventually) Google Analytics, for faster and more granular analytics over arbitrary events and event payloads; significantly improving query times from minutes to milliseconds",
"Legacy code maintenance/incremental refactoring",
"Consolidated various async jobs (e.g. notifications, daily/weekly digest, delayed release, etc.) into type-safe Temporal workflows, enhancing developer experience and setting conventions for easy implementation of future workflows",
"Designed and deployed a multi-instance tRPC server for type-safe endpoints for the frontend",
`Adapted Agile/Scrum to accomodate stakeholder expectations of simultaneous planned and unplanned (i.e. during a Sprint) throughput and turnaround, while keeping longer-term goals predictable.`,
"Crafted multi-stage Dockerfiles to leverage layer caching in various projects",
"Reduced deployment time from 5 minutes to 15 seconds by implementing locally-runnable deploy scripts, besides setting up Github Actions CI/CD pipelines.",
"Developed a custom IVR phone system for dynamically browsing our content.",
"Deployed and configured various supporting services in Kubernetes such as Grafana, private Docker & NPM registries.",
"Implemented OpenTelemetry tracing visualized in Grafana, resulting in 50% decrease in legacy transcoder troubleshooting time.",
"Designed various parameterized Grafana dashboards for at-a-glance analysis of user stats.",
"Implemented two integrations: MySQL-Clickhouse sync (using Clickhouse primitives), and MySQL-Salesforce sync (on Temporal).",
"Transitioned legacy Cloudflare/nginx TLD site to Vercel, while maintaining custom nginx routes on the TLD and hosting the entire legacy site under a subdomain.",
],
},
// {
// title: "Assistant Manager",
// company: "Sureknit Inc. Property Management",
// location: "East Flatbush, NY",
// start: "February 2020",
// end: "June 2022",
// highlights: [
// "Kept accurate records of tenant rent payments.",
// "Balanced and monitored bank accounts for all subsidiary companies.",
// "Furnished necessary documentation for various real estate operations, such as new leases, property sales, etc.",
// ],
// },
// {
// title: "Architect Intern",
// company: "Sandy Hacohen",
// location: "Flushing, NY",
// start: "July 2018",
// end: "November 2018",
// highlights: [
// "Produced architectural drawings of various points-of-view based on given floorplans.",
// "Adjusted existing floorplans to spec.",
// ],
// },
// {
// title: "Operations Manager",
// company: "Tax Opportunities America",
// location: "Flushing, NY",
// start: "April 2014",
// end: "May 2015",
// highlights: [
// "Interfaced with clients, prepared necessary documentation and submitted applications to government programs on behalf of clients.",
// "Monitored and accommodated deadlines, missing client data, and when client promised to have the data available",
// ],
// },
// {
// title: "Representative",
// company: "Computer-Aided Engineering Network (CAEN) Hotline",
// location: "U. of Michigan",
// start: "2007",
// end: "2009, 2010",
// highlights: [
// "Provided first-response assistance to students and collecting pertinent information to arrange for involvement of specialized staff.",
// ],
// },
];
export const projectsHobbies = [
{
title: "Token-Efficient AI Chat",
highlights: ["Vike", "Hono", "React", "Cloudflare Pages"],
code: "https://git.sakal.us/avraham/context-efficient-ai",
deployment: "https://ai.sakal.us",
},
{
title: "Calendar Optimizer project",
highlights: [
"Stock options research platform",
"Preact",
"trpc",
"Clickhouse",
],
code: "https://git.sakal.us/avraham/calendar-optimizer",
deployment: "https://calendar-optimizer-frontend.sakal.us",
},
{
title: "This Résumé",
highlights: ["Vike", "React", "Cloudflare Pages"],
code: "https://git.sakal.us/avraham/resume",
deployment: "https://resume.sakal.us",
},
{
title: "Personal coding blog",
highlights: ["Astro", "Cloudflare Pages"],
code: "https://git.sakal.us/avraham/blog-astro",
deployment: "https://blog.sakal.us",
},
{
title: "Self-host email for the sakal.us domain",
highlights: ["Vultr-Hosted", "Postfix for SMTP", "Dovecot for IMAP"],
},
{
title: "Self-host a Kubernetes cluster",
highlights: [
"k3s",
"Private Docker image registry",
"cert-manager",
"Private coding projects",
],
},
{
title: "Self-host a Gitea code repository",
highlights: ["Deployed within the above Kubernetes cluster."],
deployment: "https://git.sakal.us",
},
// {
// title: "Custom From-Scratch Full-Stack eCommerce website",
// highlights: [
// "Previously at piazzaoptical.com",
// "mithril.js",
// "nginx as reverse proxy and TLS termination",
// "lua-http on LuaJIT",
// "LMDB",
// ],
// },
];
export const education = [
{
title: "B.S. in Aerospace Engineering",
location: "University of Michigan, Ann Arbor, MI",
start: "Sept. 2006",
end: "April 2009, Sept. 2010, April 2011",
gpa: "3.185",
},
// {
// title: "Ph.D. in Rabbinics",
// location: "Rabbinical Seminary of America, Flushing, NY",
// start: "April 2009",
// end: "April 2010, April 2011, January 2020",
// },
];
+72
View File
@@ -0,0 +1,72 @@
import eslint from "@eslint/js";
import react from "eslint-plugin-react";
import globals from "globals";
import tseslint, { type ConfigArray } from "typescript-eslint";
export default tseslint.config(
{
ignores: [
"dist/*",
// Temporary compiled files
"**/*.ts.build-*.mjs",
// JS files at the root of the project
"*.js",
"*.cjs",
"*.mjs",
],
},
eslint.configs.recommended,
...tseslint.configs.recommended,
{
languageOptions: {
parserOptions: {
warnOnUnsupportedTypeScriptVersion: false,
sourceType: "module",
ecmaVersion: "latest",
},
},
},
{
rules: {
"@typescript-eslint/no-unused-vars": [
1,
{
argsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-namespace": 0,
},
},
{
files: ["**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}"],
...react.configs.flat.recommended,
languageOptions: {
...react.configs.flat.recommended.languageOptions,
globals: {
...globals.serviceworker,
...globals.browser,
},
},
settings: {
react: {
version: "detect",
},
},
} as ConfigArray[number],
react.configs.flat["jsx-runtime"] as ConfigArray[number],
{
rules: {
"react/no-unknown-property": [
"error",
{
ignore: ["css"],
},
],
},
},
);
-343
View File
@@ -1,343 +0,0 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body>
<section class="header">
<h2 class="lg bold">Brian Sakal</h2>
</section>
<section class="contact-info">
<ul class="inline">
<li>Passaic, NJ</li>
<li>305-930-0248</li>
<li><a href="https://git.sakal.us/">Git</a></li>
</ul>
</section>
<section class="sections">
<section class="relevant-skills">
<label>
<div class="fit-content">
<div class="bold">Skills/Technologies</div>
</div>
</label>
<div>
<ul class="inline">
<li>Docker</li>
<li>DevOps (Logging, Monitoring, Grafana, CI/CD)</li>
<li>Kubernetes</li>
<li>Node.js</li>
<li>Javascript/Typescript</li>
<li>React</li>
<li>MySQL/PostgreSQL</li>
<li>Elasticsearch</li>
<li>Clickhouse</li>
<li>Linux</li>
<li>Cloudflare</li>
<li>Redis</li>
<li>RethinkDB</li>
<li>Helm</li>
<li>nginx</li>
<li>Vercel</li>
<li>S3</li>
<li>Express.js</li>
<li>React</li>
<li>React Native</li>
<li>Next.js</li>
<li>Swagger</li>
</ul>
</div>
</section>
<section class="employment">
<label>
<div class="fit-content">
<span class="bold">Employment</span>
<span class="sm italic right">(Most recent)</span>
</div>
</label>
<div>
<ul class="no-bullet">
<li>
<div class="space-between">
<div>
<span class="bold">Senior Full-Stack Developer,</span>
<span>TorahAnytime.com, Flushing, NY</span>
</div>
<div class="italic right">June 2022Present</div>
</div>
<div>
<ul>
<!-- <li>
Fixed/troubleshooted bugs and added features across multiple
legacy codebases
</li>
<li>Consolidated cloud resources by using Kubernetes.</li> -->
<li>
Transitioned a legacy Cloudflare/nginx TLD site to Vercel,
while maintaining custom nginx routes on the TLD, plus the
entire legacy site under a subdomain.
</li>
<li>
Reduced time-to-deployment from 5 minutes to 15 seconds with
locally-runnable deploy scripts; besides setting up
Drone/Gitlab CI/CD pipelines.
</li>
<li>
Architected custom analytics backend w/ Clickhouse. Reduced
query times from minutes to milliseconds.
</li>
<li>
Implemented a custom IVR phone system to dynamically browse
content.
</li>
<li>
Deployed and configured various supporting/ancillary
services in Kubernetes, including Gitlab, private Docker &
NPM registries.
</li>
<li>
Implemented OpenTelemetry tracing visualized in Grafana,
decreasing transcoder troubleshooting by 50%.
</li>
<li>
Implemented various asynchronous workflows, including a new
video transcoder, in Temporal; thus completely severing our
dependence on Vimeo, reducing costs by $40k/year.
</li>
</ul>
</div>
</li>
<li>
<div class="space-between">
<div>
<span class="bold">Assistant Manager,</span>
<span
>Sureknit Inc. Property Management, East Flatbush, NY</span
>
</div>
<div class="italic right">February 2020June 2022</div>
</div>
<div>
<ul>
<li>Kept accurate records of tenant rent payments.</li>
<li>
Balanced and monitored bank accounts for all subsidiary
companies.
</li>
<li>
Furnished necessary documentation for various real estate
operations, such as new leases, property sales, etc.
</li>
</ul>
</div>
</li>
<!-- <li>
<div>
<span class="bold">Architect Intern,</span>
<span>Sandy Hacohen, Flushing, NY</span>
</div>
<div class="italic right">July 2018November 2018</div>
<div>
<ul>
<li>
Produced architectural drawings of various points-of-view
based on given floorplans.
</li>
<li>Adjusted existing floorplans to spec.</li>
</ul>
</div>
</li> -->
<!-- <li>
<div class="space-between">
<div>
<span class="bold">Operations Manager,</span>
<span>Tax Opportunities America, Flushing, NY</span>
</div>
<div class="italic right">April 2014May 2015</div>
</div>
<div>
<ul>
<li>
Interfaced with clients, prepared necessary documentation
and submitted applications to government programs on behalf
of clients.
</li>
<li>
Monitored and accommodated deadlines, missing client data,
and when client promised to have the data available
</li>
</ul>
</div>
</li> -->
<!-- <li>
<div class="space-between">
<div>
<span class="bold">Representative,</span>
<span
>Computer-Aided Engineering Network (CAEN) Hotline, U. of
Michigan</span
>
</div>
<div class="italic right">20072009, 20102011</div>
</div>
<div>
<ul>
<li>
Provided first-response assistance to students and
collecting pertinent information to arrange for involvement
of specialized staff.
</li>
</ul>
</div>
</li> -->
</ul>
</div>
</section>
<section class="projects-hobbies">
<label>
<div class="fit-content">
<div class="bold">Projects/Hobbies</div>
</div>
</label>
<div>
<ul class="no-bullet">
<li>
<div>
<span class="bold"
>Self-host email for the sakal.us domain</span
>
<span
><ul class="inline">
<li>Vultr-Hosted</li>
<li>Postfix for SMTP</li>
<li>Dovecot for IMAP</li>
</ul></span
>
</div>
</li>
<li>
<div>
<span class="bold">Self-host a Kubernetes cluster</span>
<span
><ul class="inline">
<li><code>k3s</code></li>
<li>Private Docker image registry</li>
<li>
<code>cert-manager</code>
</li>
<li>Private coding projects</li>
</ul></span
>
</div>
</li>
<li>
<div>
<span class="bold">Self-host a Gitea code repository</span>
<span
>Deployed within the above Kubernetes cluster. Available at
<a href="https://git.sakal.us/">https://git.sakal.us</a></span
>
</div>
</li>
<li>
<div>
<span class="bold"
>Personal coding blog (<a
href="https://git.sakal.us/avraham/blog-astro"
>Code</a
>)</span
>
<span
><ul class="inline">
<li>Astro</li>
<li>Cloudflare Pages</li>
<li>
<a href="https://blog.sakal.us/">https://blog.sakal.us</a>
</li>
</ul>
</span>
</div>
</li>
<li>
<div>
<span class="bold"
>Calendar Optimizer project (<a
href="https://git.sakal.us/avraham/calendar-optimizer"
>Code</a
>)</span
>
<span
><ul class="inline">
<li>Stock options research platform</li>
<li>Preact</li>
<li>trpc</li>
<li>Clickhouse</li>
<li>
<a href="https://calendar-optimizer-frontend.sakal.us"
>https://calendar-optimizer-frontend.sakal.us</a
>
</li>
</ul></span
>
</div>
</li>
<li>
<div>
<span class="bold"
>Custom From-Scratch Full-Stack eCommerce website (<a
href="https://git.sakal.us/avraham/piazzaoptical.com"
>Code</a
>)</span
>
<span
><ul class="inline">
<li>Previously at piazzaoptical.com</li>
<li>
<a href="https://mithril.js.org/">mithril.js</a>
</li>
<li>nginx as reverse proxy and TLS termination</li>
<li>
<a href="https://daurnimator.github.io/lua-http/0.4/"
>lua-http</a
>
on <a href="https://luajit.org/">LuaJIT</a>
</li>
<li><a href="http://www.lmdb.tech/doc/">LMDB</a></li>
</ul></span
>
</div>
</li>
</ul>
</div>
</section>
<section class="education">
<label>
<span class="bold">Education</span>
</label>
<div>
<ul class="no-bullet">
<li>
<div class="space-between">
<div>
<span class="bold">B.S. in Aerospace Engineering,</span>
<span>University of Michigan, Ann Arbor, MI</span>
</div>
<div class="italic right">
Sept. 2006April 2009, Sept. 2010April 2011, GPA 3.185
</div>
</div>
</li>
<!-- <li>
<div>
<span class="bold">Ph.D. in Rabbinics,</span>
<span>Rabbinical Seminary of America, Flushing, NY</span>
</div>
<div class="italic right">
April 2009April 2010, April 2011January 2020
</div>
</li> -->
</ul>
</div>
</section>
</section>
</body>
</html>
+19
View File
@@ -0,0 +1,19 @@
import "./style.css";
export default function LayoutDefault({
children,
}: {
children: React.ReactNode;
}) {
return (
<div
css={{
display: "flex",
maxWidth: 1024,
margin: "auto",
}}
>
{children}
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
/* Reset */
body {
margin: 0;
font-family: sans-serif;
}
* {
box-sizing: border-box;
}
-5
View File
@@ -1,5 +0,0 @@
@import url("./common.css");
@page {
size: letter;
margin: 0.5in;
}
+39
View File
@@ -0,0 +1,39 @@
{
"scripts": {
"dev": "vike dev",
"build": "vike build",
"preview": "run-s build preview:wrangler",
"generate-pdf": "concurrently -k -s first 'pnpm vike preview' 'wait-on http://localhost:3000 && chromium --headless --no-pdf-header-footer --print-to-pdf=./assets/resume-brian-sakal.pdf --timeout=1500 http://localhost:3000' && cp ./assets/resume-brian-sakal.pdf ./dist/client/assets/static/resume-brian-sakal.pdf",
"lint": "eslint .",
"preview:wrangler": "wrangler pages dev",
"deploy:wrangler": "wrangler pages deploy",
"deploy": "run-s build generate-pdf deploy:wrangler"
},
"dependencies": {
"@compiled/react": "^0.18.6",
"@vitejs/plugin-react": "^5.0.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"vike": "^0.4.237",
"vike-cloudflare": "^0.1.7",
"vike-react": "^0.6.5"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250927.0",
"@eslint/js": "^9.33.0",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"concurrently": "^9.2.1",
"eslint": "^9.33.0",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
"npm-run-all2": "^8.0.4",
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2",
"vite-plugin-compiled-react": "^1.3.1",
"wait-on": "^9.0.1",
"wrangler": "^4.40.2"
},
"type": "module"
}
+17
View File
@@ -0,0 +1,17 @@
import type { Config } from "vike/types";
import vikeReact from "vike-react/config";
import Layout from "../layouts/LayoutDefault.js";
// Default config (can be overridden by pages)
// https://vike.dev/config
export default {
// https://vike.dev/Layout
Layout,
// https://vike.dev/head-tags
title: "Brian Sakal | Resume",
description: "Resume Auto-Generated from Metadata",
extends: vikeReact,
} satisfies Config;
+19
View File
@@ -0,0 +1,19 @@
import { usePageContext } from "vike-react/usePageContext";
export default function Page() {
const { is404 } = usePageContext();
if (is404) {
return (
<>
<h1>404 Page Not Found</h1>
<p>This page could not be found.</p>
</>
);
}
return (
<>
<h1>500 Internal Server Error</h1>
<p>Something went wrong.</p>
</>
);
}
+266
View File
@@ -0,0 +1,266 @@
import { useData } from "vike-react/useData";
import type { Data } from "./+data";
import "../../styles.css";
import "./print-button.css";
import resumeImage from "../../assets/resume-image.jpg";
export default function Page() {
return (
<div
css={{
fontFamily: '"EB Garamond", serif',
fontOpticalSizing: "auto",
fontWeight: 400,
fontStyle: "normal",
fontSize: "0.85em",
display: "flex",
flexDirection: "row",
justifyContent: "start",
}}
>
<div
css={{
display: "flex",
flexDirection: "column",
width: "30%",
backgroundColor: "lavender",
padding: "1em",
gap: "1em",
}}
>
<Header />
<MainSkills />
<ExposedSkills />
</div>
<div
css={{
display: "flex",
flexDirection: "column",
width: "70%",
paddingLeft: "1em",
paddingRight: "1em",
position: "relative",
}}
>
<div
css={{
display: "flex",
flexDirection: "row",
justifyContent: "center",
width: "100%",
position: "absolute",
}}
>
<a
className="floating-print-btn"
href={/*pdfUrl*/ "assets/static/resume-brian-sakal.pdf"}
download
>
{/* Print */}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 640"
width="24"
height="24"
>
{/*Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.*/}
<path d="M128 128C128 92.7 156.7 64 192 64L405.5 64C422.5 64 438.8 70.7 450.8 82.7L493.3 125.2C505.3 137.2 512 153.5 512 170.5L512 208L128 208L128 128zM64 320C64 284.7 92.7 256 128 256L512 256C547.3 256 576 284.7 576 320L576 416C576 433.7 561.7 448 544 448L512 448L512 512C512 547.3 483.3 576 448 576L192 576C156.7 576 128 547.3 128 512L128 448L96 448C78.3 448 64 433.7 64 416L64 320zM192 480L192 512L448 512L448 416L192 416L192 480zM520 336C520 322.7 509.3 312 496 312C482.7 312 472 322.7 472 336C472 349.3 482.7 360 496 360C509.3 360 520 349.3 520 336z" />
</svg>
</a>
</div>
<Employment />
<ProjectsHobbies />
<Education />
</div>
</div>
);
}
function Header() {
const { contactInfo } = useData<Data>();
return (
<div
css={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
<section>
<div css={{ display: "flex", justifyContent: "center" }}>
<img
src={resumeImage}
alt="headshot"
css={{ width: "70%", borderRadius: "50%", objectFit: "contain" }}
/>
</div>
</section>
<section>
<h2 css={{ marginBottom: "0.3em" }} className="lg bold">
{contactInfo.name}
</h2>
</section>
<section css={{ textAlign: "center" }}>
<ul className="inline">
<li>{contactInfo.phone}</li>
<li>{contactInfo.email}</li>
</ul>
<ul className="inline">
<li>{contactInfo.location}</li>
<li>
<a href={contactInfo.website}>{contactInfo.website}</a>
</li>
</ul>
</section>
</div>
);
}
function MainSkills() {
const { mainSkills } = useData<Data>();
return (
<section className="relevant-skills">
<h3>Main Skills/Technologies</h3>
<div>
<ul className="inline" css={{ textAlign: "justify" }}>
{mainSkills.map((skill) => (
<li>{skill}</li>
))}
</ul>
</div>
</section>
);
}
function ExposedSkills() {
const { exposedSkills } = useData<Data>();
return (
<section className="relevant-skills">
<h3>Have Used</h3>
<div>
<ul className="inline" css={{ textAlign: "justify" }}>
{exposedSkills.map((skill) => (
<li>{skill}</li>
))}
</ul>
</div>
</section>
);
}
function Employment() {
const { employment } = useData<Data>();
return (
<section className="employment">
<h2>
<div className="fit-content">
<span className="bold">Employment </span>
<span css={{ fontWeight: 300 }} className="sm italic right">
(Most recent)
</span>
</div>
</h2>
<div>
<ul className="no-bullet">
{employment.map((employment) => (
<li>
<div className="space-between" key={employment.title}>
<div>
<span className="bold">{employment.title},</span>{" "}
<span>
{employment.company}, {employment.location}
</span>
</div>
<div className="italic right">
{employment.start}-{employment.end}
</div>
</div>
<div>
<ul>
{employment.highlights.map((highlight) => (
<li key={highlight} css={{ textAlign: "justify" }}>
{highlight}
</li>
))}
</ul>
</div>
</li>
))}
</ul>
</div>
</section>
);
}
function ProjectsHobbies() {
const { projectsHobbies } = useData<Data>();
return (
<section className="projects-hobbies">
<h2>
<div className="fit-content">
<div className="bold">Projects/Hobbies</div>
</div>
</h2>
<div>
<ul className="no-bullet">
{projectsHobbies.map((projectHobby) => (
<li>
<div>
<span className="bold">{projectHobby.title}</span>
<span>
<ul className="inline">
{projectHobby.highlights.map((highlight) => (
<li>{highlight}</li>
))}
{projectHobby.deployment && (
<li>
<a href={projectHobby.deployment}>
{projectHobby.deployment}
</a>
</li>
)}
{projectHobby.code && (
<li>
<a href={projectHobby.code}>[Code]</a>
</li>
)}
</ul>
</span>
</div>
</li>
))}
</ul>
</div>
</section>
);
}
function Education() {
const { education } = useData<Data>();
return (
<section className="education">
<h2>
<span className="bold">Education</span>
</h2>
<div>
<ul className="no-bullet">
{education.map((education) => (
<li>
<div className="space-between">
<div>
<span className="bold">{education.title},</span>
<span>{education.location}</span>
</div>
<div className="italic right">
{education.start}{education.end}
</div>
</div>
</li>
))}
</ul>
</div>
</section>
);
}
+7
View File
@@ -0,0 +1,7 @@
import * as resumeData from "../../database/resume.js";
export default function data() {
return resumeData;
}
export type Data = ReturnType<typeof data>;
+54
View File
@@ -0,0 +1,54 @@
.floating-print-btn {
/* position: fixed;
bottom: 24px;
right: 24px; */
position: absolute;
width: 56px;
height: 56px;
border-radius: 50%;
background: #f8f9fa;
/* border: none; */
border: 1px solid #e0e0e0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
z-index: 1000;
animation: float 2.2s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-6px);
}
}
.floating-print-btn:hover {
background: #f0f2f5;
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2), 0 4px 8px rgba(0, 0, 0, 0.15);
}
.floating-print-btn:active {
background: #f8f9fa;
transform: translateY(0);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1);
}
.floating-print-btn {
color: #444444;
}
@media print {
.floating-print-btn {
display: none;
}
}
+5565
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -1,2 +0,0 @@
#!/bin/sh
miniserve --index index.html .
+9 -2
View File
@@ -1,5 +1,3 @@
@import url("./letter.css");
body { body {
font-family: "EB Garamond", serif; font-family: "EB Garamond", serif;
font-optical-sizing: auto; font-optical-sizing: auto;
@@ -102,3 +100,12 @@ a {
font-weight: 500; font-weight: 500;
text-decoration: none; text-decoration: none;
} }
.employment > :first-child {
margin-block-start: 0.3em;
}
h2 {
margin-block-start: 0.3em;
margin-block-end: 0.3em;
}
+29
View File
@@ -0,0 +1,29 @@
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"module": "ESNext",
"noEmit": true,
"moduleResolution": "Bundler",
"target": "ES2022",
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
],
"types": [
"vite/client",
"vike-react",
"vike-cloudflare/types"
],
"jsx": "react-jsx",
"jsxImportSource": "react"
},
"exclude": [
"dist"
]
}
+19
View File
@@ -0,0 +1,19 @@
import { pages } from "vike-cloudflare";
import react from "@vitejs/plugin-react";
import { compiled } from "vite-plugin-compiled-react";
import vike from "vike/plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
vike(),
compiled({
extract: true,
}),
react(),
pages(),
],
build: {
target: "es2022",
},
});
+4
View File
@@ -0,0 +1,4 @@
name = "resume"
compatibility_date = "2024-09-29"
pages_build_output_dir = "./dist/cloudflare"
compatibility_flags = [ "nodejs_compat" ]