Started refactoring, added prettier and checks and reformatted project, added cspell and checks and custom project words, beginning of robotic oceanographic surface sampler content
Some checks failed
Build and Test - Staging / test (pull_request) Failing after 2m35s
Build and Test - Staging / build_and_push (pull_request) Has been skipped
Build and Test - Staging / deploy_staging (pull_request) Has been skipped

This commit is contained in:
2025-11-30 15:48:36 -08:00
parent 67eb549ed2
commit 4a59e44716
83 changed files with 3246 additions and 1643 deletions

View File

@@ -1,7 +1,17 @@
.DS_Store .DS_Store
.idea .gitea/
.astro .astro/
.idea/
*/dist/
*/build/ */build/
*/node_modules/ */node_modules/
*/playwright-report/
*/test-results/
.gitignore
Dockerfile
Makefile
new-words.txt
README.md

View File

@@ -1,9 +1,9 @@
name: Playwright Tests name: Playwright Tests
on: on:
push: push:
branches: [ main, master ] branches: [main, master]
pull_request: pull_request:
branches: [ main, master ] branches: [main, master]
jobs: jobs:
test: test:
timeout-minutes: 60 timeout-minutes: 60

View File

@@ -1,7 +1,7 @@
name: Build and Test - Production name: Build and Test - Production
on: on:
push: push:
branches: [ main ] branches: [main]
jobs: jobs:
test: test:
@@ -22,6 +22,12 @@ jobs:
npm ci npm ci
npx playwright install --with-deps npx playwright install --with-deps
- name: Code Formatting Check
run: npx prettier . --check
- name: Spelling Check
run: npx cspell .
- name: Build Project - name: Build Project
run: npm run build run: npm run build
@@ -98,4 +104,4 @@ jobs:
-H 'accept: */*' \ -H 'accept: */*' \
-H 'Authorization: Bearer ${{ secrets.TRUENAS_CAPERRENCOM_API_KEY }}' \ -H 'Authorization: Bearer ${{ secrets.TRUENAS_CAPERRENCOM_API_KEY }}' \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '"caperren-com"' -d '"caperren-com"'

View File

@@ -1,7 +1,7 @@
name: Build and Test - Staging name: Build and Test - Staging
on: on:
pull_request: pull_request:
types: [ opened, synchronize, reopened ] types: [opened, synchronize, reopened]
jobs: jobs:
test: test:
@@ -24,6 +24,12 @@ jobs:
npm ci npm ci
npx playwright install --with-deps npx playwright install --with-deps
- name: Code Formatting Check
run: npx prettier . --check
- name: Spelling Check
run: npx cspell .
- name: Build Project - name: Build Project
run: npm run build run: npm run build
@@ -100,4 +106,4 @@ jobs:
-H 'accept: */*' \ -H 'accept: */*' \
-H 'Authorization: Bearer ${{ secrets.TRUENAS_CAPERRENCOM_API_KEY }}' \ -H 'Authorization: Bearer ${{ secrets.TRUENAS_CAPERRENCOM_API_KEY }}' \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '"caperren-com-stg"' -d '"caperren-com-stg"'

9
.gitignore vendored
View File

@@ -1,9 +1,3 @@
# Ignore everything under src/content, as they are dynamically added from obsidian
src/content/*
# Do not ignore config.ts in src/content since that necessary to import the dynamic content
!src/content/config.ts
# build output # build output
dist/ dist/
@@ -35,3 +29,6 @@ pnpm-debug.log*
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
/playwright/.auth/ /playwright/.auth/
# Local temporary storage files
new-words.txt

34
.prettierignore Normal file
View File

@@ -0,0 +1,34 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
# Local temporary storage files
new-words.txt

11
.prettierrc Normal file
View File

@@ -0,0 +1,11 @@
{
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
]
}

View File

@@ -5,12 +5,27 @@ WORKDIR /app
# Therefore, the `-deps` steps will be skipped if only the source code changes. # Therefore, the `-deps` steps will be skipped if only the source code changes.
COPY package.json package-lock.json tsconfig.json astro.config.mjs ./ COPY package.json package-lock.json tsconfig.json astro.config.mjs ./
CMD [ "/bin/bash" ]
FROM base AS prod-deps FROM base AS prod-deps
RUN npm install --omit=dev RUN npm install --omit=dev
FROM prod-deps AS test-base
RUN npm ci
RUN npx playwright install --with-deps
FROM prod-deps AS build FROM prod-deps AS build
COPY . . COPY --exclude=test \
--exclude=test-e2e \
--exclude=playwright.config.ts \
--exclude=vitest.config.ts \
--exclude=.prettierrc \
--exclude=.prettierignore \
--exclude=cspell.json \
--exclude=project-words.txt \
. .
ARG REPO_VERSION_HASH ARG REPO_VERSION_HASH
ARG BUILD_ENVIRONMENT ARG BUILD_ENVIRONMENT
@@ -19,6 +34,16 @@ RUN echo "PUBLIC_REPO_VERSION_HASH=\"${REPO_VERSION_HASH}\" \n\
PUBLIC_BUILD_ENVIRONMENT=\"${BUILD_ENVIRONMENT}\"" >> .env PUBLIC_BUILD_ENVIRONMENT=\"${BUILD_ENVIRONMENT}\"" >> .env
RUN npm run build RUN npm run build
FROM test-base AS test
COPY . .
COPY --from=build /app/dist /app/dist
RUN npx prettier . --check
RUN npx cspell .
RUN npm run test
RUN npm run e2e-test
FROM nginx:alpine AS runtime FROM nginx:alpine AS runtime
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf COPY ./nginx/nginx.conf /etc/nginx/nginx.conf

View File

@@ -6,7 +6,14 @@
astro_upgrade \ astro_upgrade \
build \ build \
dev \ dev \
dev-hosted dev-hosted \
test \
_spelling-generate-new-words \
spelling-find-new-words \
spelling-add-new-words \
spelling-check \
cleanup-check \
cleanup-code
default: dev default: dev
@@ -28,3 +35,30 @@ dev:
dev-hosted: dev-hosted:
npm run dev-hosted npm run dev-hosted
test: spelling-check
@npx playwright install --with-deps
npm run test --ui
npx playwright test --ui
_spelling-generate-new-words:
@cspell --words-only --unique . 2>/dev/null | sort --ignore-case -o new-words.txt
spelling-find-new-words: _spelling-generate-new-words
@echo "Found the following new words:"
@cat new-words.txt
@rm -f new-words.txt
spelling-add-new-words: _spelling-generate-new-words
@echo "Adding to project-words.txt"
@cat new-words.txt >> project-words.txt
@rm -f new-words.txt
@cat project-words.txt | sort --ignore-case -o project-words.txt
spelling-check:
npx cspell .
cleanup-check:
npx prettier . --check
cleanup-code:
npx prettier . --write

View File

@@ -1,3 +1,3 @@
# Corwin Perren's Personal Portfolio Website # Corwin Perren's Personal Portfolio Website
Check the Makfile and/or package.json for the commands needed to build and run this project. Check the Makefile and/or package.json for the commands needed to build and run this project.

View File

@@ -1,31 +1,31 @@
// @ts-check // @ts-check
import {defineConfig} from 'astro/config'; import { defineConfig } from "astro/config";
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
// We don't have access to short imports this early in the build chain // We don't have access to short imports this early in the build chain
// noinspection ES6PreferShortImport // noinspection ES6PreferShortImport
import {siteLayout, getPaths} from "./src/data/site-layout.ts"; import { siteLayout, getPaths } from "./src/data/site-layout.ts";
const disabledPaths = getPaths(siteLayout, [], true) const disabledPaths = getPaths(siteLayout, [], true);
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: "https://caperren.com", site: "https://caperren.com",
prefetch: { prefetch: {
prefetchAll: true prefetchAll: true,
}, },
integrations: [ integrations: [
sitemap({ sitemap({
filter: (pagePath) => filter: (pagePath) =>
!disabledPaths.some(disabledPath => pagePath.includes(disabledPath)) !disabledPaths.some((disabledPath) => pagePath.includes(disabledPath)),
}) }),
],
vite: {
plugins: [
// @ts-ignore
tailwindcss(),
], ],
vite: { },
plugins: [ });
// @ts-ignore
tailwindcss()
],
},
});

23
cspell.json Normal file
View File

@@ -0,0 +1,23 @@
{
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
"version": "0.2",
"dictionaryDefinitions": [
{
"name": "project-words",
"path": "./project-words.txt",
"addWords": true
}
],
"dictionaries": ["project-words"],
"ignorePaths": [
".astro",
".idea",
"dist",
"node_modules",
"playwright-report",
"test-results",
"new-words.txt",
"playwright.config.ts",
"/project-words.txt"
]
}

1091
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,7 @@
"@astrojs/sitemap": "^3.6.0", "@astrojs/sitemap": "^3.6.0",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"astro": "^5.16.3", "astro": "^5.16.3",
"cspell": "^9.3.2",
"flowbite": "^3.1.2", "flowbite": "^3.1.2",
"leader-line-new": "^1.1.9", "leader-line-new": "^1.1.9",
"luxon": "^3.7.2", "luxon": "^3.7.2",
@@ -25,6 +26,9 @@
"@playwright/test": "^1.56.1", "@playwright/test": "^1.56.1",
"@types/luxon": "^3.7.1", "@types/luxon": "^3.7.1",
"@types/node": "^24.10.0", "@types/node": "^24.10.0",
"prettier": "3.7.3",
"prettier-plugin-astro": "0.14.1",
"prettier-plugin-tailwindcss": "0.7.1",
"vitest": "^4.0.7" "vitest": "^4.0.7"
} }
} }

View File

@@ -1,4 +1,4 @@
import {defineConfig, devices} from '@playwright/test'; import { defineConfig, devices } from "@playwright/test";
/** /**
* Read environment variables from file. * Read environment variables from file.
@@ -12,69 +12,69 @@ import {defineConfig, devices} from '@playwright/test';
* See https://playwright.dev/docs/test-configuration. * See https://playwright.dev/docs/test-configuration.
*/ */
export default defineConfig({ export default defineConfig({
testDir: './test-e2e', testDir: "./test-e2e",
/* Run tests in files in parallel */ /* Run tests in files in parallel */
fullyParallel: true, fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */ /* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
/* Retry on CI only */ /* Retry on CI only */
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html', reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('')`. */ /* Base URL to use in actions like `await page.goto('')`. */
baseURL: 'http://localhost:4321', baseURL: "http://localhost:4321",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
}, },
/* Configure projects for major browsers */ {
projects: [ name: "firefox",
{ use: { ...devices["Desktop Firefox"] },
name: 'chromium',
use: {...devices['Desktop Chrome']},
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
//
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */
{
name: 'Mobile Chrome',
use: {...devices['Pixel 5']},
},
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run preview',
url: 'http://localhost:4321',
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
}, },
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
/* Test against mobile viewports. */
{
name: "Mobile Chrome",
use: { ...devices["Pixel 5"] },
},
{
name: "Mobile Safari",
use: { ...devices["iPhone 12"] },
},
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: "npm run preview",
url: "http://localhost:4321",
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
},
}); });

41
project-words.txt Normal file
View File

@@ -0,0 +1,41 @@
ASSEM
astrojs
Candian
caperren
CEOAS
COMSC
Concours
CONSERV
Corwin
dangerousthings
Dechorionator
fhhs
flowbite
HDFS
headshot
Homelab
ITAR
Jetson
leconte
Loctite
luxon
MGMT
nixos
Onshape
OSSM
OSURC
Perren
Perren's
pubpath
RFID
RSSI
SARL
Shuttlebox
sinnhuber
sitemapindex
ssds
Starlink
Unstow
vitest
Zebrafish
zscan

View File

@@ -1,117 +0,0 @@
---
import {Image} from 'astro:assets';
import type {carouselGroup} from "@interfaces/image-carousel.ts";
const groupToShow: carouselGroup = Astro.props.carouselGroup;
---
<custom-carousel class="flex flex-col relative w-full"
data-custom-carousel={groupToShow.animation}
data-custom-carousel-interval={groupToShow.interval}>
<!-- Modal for fullscreen viewing -->
<div tabindex="-1" aria-hidden="true"
class="hidden fixed z-100 justify-center items-center w-full inset-0"
data-custom-carousel-modal>
<div class="relative p-4 w-full h-full">
<!-- Modal content -->
<div class="relative h-full max-h-screen max-w-screen bg-black rounded-lg shadow-sm border-2 border-caperren-green">
<!-- Modal header -->
<div class="flex items-center justify-between p-1 rounded-t">
<button type="button"
class="z-100 text-caperren-green bg-transparent ring-2 ring-caperren-green-dark hover:ring-caperren-green-light hover:text-caperren-green-light rounded-lg text-sm size-8 ms-auto inline-flex justify-center items-center"
data-custom-carousel-modal-hide>
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<div class="flex items-center justify-center overflow-hidden h-full w-full"
data-custom-carousel-modal-image/>
</div>
</div>
</div>
<!-- Carousel wrapper -->
<div class="relative overflow-hidden w-full h-56 md:h-120">
{
groupToShow.images.map((image, index) => (
<div class="hidden duration-1500 ease-in-out" data-custom-carousel-item={index}>
<Image src={image}
class="absolute inset-0 m-auto h-full w-auto max-h-full max-w-full object-contain"
alt="..."
layout='constrained'
loading="eager"/>
</div>
))
}
</div>
{(groupToShow.images.length > 1) && (
<!-- Slider indicators -->
<div class="absolute z-30 flex -translate-x-1/2 bottom-2 left-1/2 space-x-3 rounded-full">
{
groupToShow.images.map((_, index) => (
<button type="button"
class="w-3 h-3 rounded-full bg-black hover:bg-caperren-green-light"
aria-current={index ? "false" : "true"}
aria-label={index.toString()}
data-custom-carousel-slide-to={index}></button>
))
}
</div>
<!-- Slider controls -->
<button
type="button"
class="absolute top-0 start-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-none"
data-custom-carousel-prev
>
<span class="inline-flex items-center justify-center w-10 h-10 rounded-full ring-2 ring-caperren-green/25 bg-black/25 group-hover:bg-black/75">
<svg
class="h-4 w-4 text-caperren-green group-hover:text-caperren-green-light"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 1 1 5l4 4"
/>
</svg>
<span class="hidden">Previous</span>
</span>
</button>
<button
type="button"
class="absolute top-0 end-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-none"
data-custom-carousel-next
>
<span class="inline-flex items-center justify-center w-10 h-10 rounded-full ring-2 ring-caperren-green/25 bg-black/25 group-hover:bg-black/75">
<svg
class="h-4 w-4 text-caperren-green group-hover:text-caperren-green-light "
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m1 9 4-4-4-4"
/>
</svg>
<span class="hidden">Next</span>
</span>
</button>
)}
</custom-carousel>
<script src="./custom-carousel.ts"/>

View File

@@ -1,100 +0,0 @@
import {
Carousel,
type CarouselItem,
type CarouselInterface,
type CarouselOptions,
type IndicatorItem,
Modal,
type ModalInterface,
} from 'flowbite';
class CustomCarousel extends HTMLElement {
_slide: boolean;
_items: CarouselItem[];
_options: CarouselOptions;
_carousel: CarouselInterface;
_modalEl: HTMLElement;
_modal: ModalInterface;
constructor() {
super();
this._slide = this.getAttribute('data-custom-carousel') === 'slide';
this._items = this._getItems();
this._options = this._getOptions();
this._carousel = new Carousel(this, this._items, this._options);
if (this._slide && this._items.length > 0) this._carousel.cycle();
this._modalEl = this.querySelector('[data-custom-carousel-modal]') as HTMLElement || undefined;
this._modal = new Modal(this._modalEl);
window.addEventListener("load", this._attachHandlers);
}
_getItems = (): CarouselItem[] => {
let customItems = this.querySelectorAll('[data-custom-carousel-item]') || [];
return Array.from(customItems).map(
(item): CarouselItem => {
return {
el: item as HTMLElement,
position: Number(item.getAttribute("data-custom-carousel-item"))
}
}
)
}
_getOptions = (): CarouselOptions => {
let customIndicators = this.querySelectorAll('[data-custom-carousel-slide-to]') || [];
return {
defaultPosition: 0,
interval: this.dataset.customCarouselInterval ? Number(this.dataset.customCarouselInterval) : 8000,
indicators: {
activeClasses: 'border-2 border-caperren-green bg-black',
inactiveClasses: 'bg-caperren-green/40 hover:bg-caperren-green-light',
items: Array.from(customIndicators).map(
(item, index): IndicatorItem => {
return {
el: item as HTMLElement,
position: Number(item.getAttribute('data-custom-carousel-slide-to'))
}
}
)
}
}
}
_attachHandlers = (): void => {
// Carousel controls
this.querySelector(
'[data-custom-carousel-next]'
)?.addEventListener('click', () => this._carousel.next());
this.querySelector(
'[data-custom-carousel-prev]'
)?.addEventListener('click', () => this._carousel.prev());
// Close fullscreen modal
this._modalEl.querySelector('[data-custom-carousel-modal-hide]')?.addEventListener('click', () => this._modal.hide())
// Click to open fullscreen modal
this._items.forEach((item) => {
item.el.addEventListener('click', () => {
const imgCloned = item.el.querySelector('img')?.cloneNode() as Node;
const imageDiv = this._modalEl.querySelector('[data-custom-carousel-modal-image]') as HTMLElement || undefined;
imageDiv.innerHTML = '';
imageDiv?.appendChild(imgCloned);
this._modal.show();
})
})
}
}
customElements.define('custom-carousel', CustomCarousel)

View File

@@ -0,0 +1,5 @@
---
---
<h2 class="my-4 font-bold md:text-2xl">{Astro.props.text}</h2>

View File

@@ -0,0 +1,5 @@
---
---
<h3 class="my-4 font-bold md:text-lg">{Astro.props.text}</h3>

View File

@@ -1,7 +1,10 @@
--- ---
---
<footer class="z-50 w-full max-w-full px-6 py-2 bg-black border-t border-t-caperren-green-dark text-caperren-green-dark text-sm flex items-center justify-between"> ---
<span>{import.meta.env.PUBLIC_BUILD_ENVIRONMENT || "development"}</span>
<span>{import.meta.env.PUBLIC_REPO_VERSION_HASH || "invalid"}</span> <footer
class="border-t-caperren-green-dark text-caperren-green-dark z-50 flex w-full max-w-full items-center justify-between border-t bg-black px-6 py-2 text-sm"
>
<span>{import.meta.env.PUBLIC_BUILD_ENVIRONMENT || "development"}</span>
<span>{import.meta.env.PUBLIC_REPO_VERSION_HASH || "invalid"}</span>
</footer> </footer>

View File

@@ -0,0 +1,11 @@
---
---
<a
class="text-caperren-green border-caperren-green hover:border-caperren-green-light hover:text-caperren-green-light rounded-2xl border-2 bg-black p-2"
href={Astro.props.href}
target={Astro.props.target || "_blank"}
>
{Astro.props.title}
</a>

View File

@@ -0,0 +1,145 @@
---
import { Image } from "astro:assets";
import type { carouselGroup } from "@interfaces/image-carousel.ts";
const groupToShow: carouselGroup = Astro.props.carouselGroup;
---
<custom-carousel
class="relative flex w-full flex-col"
data-custom-carousel={groupToShow.animation}
data-custom-carousel-interval={groupToShow.interval}
>
<!-- Modal for fullscreen viewing -->
<div
tabindex="-1"
aria-hidden="true"
class="fixed inset-0 z-100 hidden w-full items-center justify-center"
data-custom-carousel-modal
>
<div class="relative h-full w-full p-4">
<!-- Modal content -->
<div
class="border-caperren-green relative h-full max-h-screen max-w-screen rounded-lg border-2 bg-black shadow-sm"
>
<!-- Modal header -->
<div class="flex items-center justify-between rounded-t p-1">
<button
type="button"
class="text-caperren-green ring-caperren-green-dark hover:ring-caperren-green-light hover:text-caperren-green-light z-100 ms-auto inline-flex size-8 items-center justify-center rounded-lg bg-transparent text-sm ring-2"
data-custom-carousel-modal-hide
>
<svg
class="h-3 w-3"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 14"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"></path>
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<div
class="flex h-full w-full items-center justify-center overflow-hidden"
data-custom-carousel-modal-image
>
</div>
</div>
</div>
</div>
<!-- Carousel wrapper -->
<div class="relative h-56 w-full overflow-hidden md:h-120">
{
groupToShow.images.map((image, index) => (
<div
class="hidden duration-1500 ease-in-out"
data-custom-carousel-item={index}
>
<Image
src={image}
class="absolute inset-0 m-auto h-full max-h-full w-auto max-w-full object-contain"
alt="..."
layout="constrained"
loading="eager"
/>
</div>
))
}
</div>
<!-- Slider indicators -->
{
groupToShow.images.length > 1 && (
<div>
<div class="absolute bottom-2 left-1/2 z-30 flex -translate-x-1/2 space-x-3 rounded-full">
{groupToShow.images.map((_, index) => (
<button
type="button"
class="hover:bg-caperren-green-light h-3 w-3 rounded-full bg-black"
aria-current={index ? "false" : "true"}
aria-label={index.toString()}
data-custom-carousel-slide-to={index}
/>
))}
</div>
<button
type="button"
class="group absolute start-0 top-0 z-30 flex h-full cursor-pointer items-center justify-center px-4 focus:outline-none"
data-custom-carousel-prev
>
<span class="ring-caperren-green/25 inline-flex h-10 w-10 items-center justify-center rounded-full bg-black/25 ring-2 group-hover:bg-black/75">
<svg
class="text-caperren-green group-hover:text-caperren-green-light h-4 w-4"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 1 1 5l4 4"
/>
</svg>
<span class="hidden">Previous</span>
</span>
</button>
<button
type="button"
class="group absolute end-0 top-0 z-30 flex h-full cursor-pointer items-center justify-center px-4 focus:outline-none"
data-custom-carousel-next
>
<span class="ring-caperren-green/25 inline-flex h-10 w-10 items-center justify-center rounded-full bg-black/25 ring-2 group-hover:bg-black/75">
<svg
class="text-caperren-green group-hover:text-caperren-green-light h-4 w-4"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m1 9 4-4-4-4"
/>
</svg>
<span class="hidden">Next</span>
</span>
</button>
</div>
)
}
</custom-carousel>
<script src="./custom-carousel.ts"></script>

View File

@@ -0,0 +1,111 @@
import {
Carousel,
type CarouselItem,
type CarouselInterface,
type CarouselOptions,
type IndicatorItem,
Modal,
type ModalInterface,
} from "flowbite";
class CustomCarousel extends HTMLElement {
_slide: boolean;
_items: CarouselItem[];
_options: CarouselOptions;
_carousel: CarouselInterface;
_modalEl: HTMLElement;
_modal: ModalInterface;
constructor() {
super();
this._slide = this.getAttribute("data-custom-carousel") === "slide";
this._items = this._getItems();
this._options = this._getOptions();
this._carousel = new Carousel(this, this._items, this._options);
if (this._slide && this._items.length > 0) this._carousel.cycle();
this._modalEl =
(this.querySelector("[data-custom-carousel-modal]") as HTMLElement) ||
undefined;
this._modal = new Modal(this._modalEl);
window.addEventListener("load", this._attachHandlers);
}
_getItems = (): CarouselItem[] => {
let customItems =
this.querySelectorAll("[data-custom-carousel-item]") || [];
return Array.from(customItems).map((item): CarouselItem => {
return {
el: item as HTMLElement,
position: Number(item.getAttribute("data-custom-carousel-item")),
};
});
};
_getOptions = (): CarouselOptions => {
let customIndicators =
this.querySelectorAll("[data-custom-carousel-slide-to]") || [];
return {
defaultPosition: 0,
interval: this.dataset.customCarouselInterval
? Number(this.dataset.customCarouselInterval)
: 8000,
indicators: {
activeClasses: "border-2 border-caperren-green bg-black",
inactiveClasses: "bg-caperren-green/40 hover:bg-caperren-green-light",
items: Array.from(customIndicators).map(
(item): IndicatorItem => {
return {
el: item as HTMLElement,
position: Number(
item.getAttribute("data-custom-carousel-slide-to"),
),
};
},
),
},
};
};
_attachHandlers = (): void => {
// Carousel controls
this.querySelector("[data-custom-carousel-next]")?.addEventListener(
"click",
() => this._carousel.next(),
);
this.querySelector("[data-custom-carousel-prev]")?.addEventListener(
"click",
() => this._carousel.prev(),
);
// Close fullscreen modal
this._modalEl
.querySelector("[data-custom-carousel-modal-hide]")
?.addEventListener("click", () => this._modal.hide());
// Click to open fullscreen modal
this._items.forEach((item) => {
item.el.addEventListener("click", () => {
const imgCloned = item.el.querySelector("img")?.cloneNode() as Node;
const imageDiv =
(this._modalEl.querySelector(
"[data-custom-carousel-modal-image]",
) as HTMLElement) || undefined;
imageDiv.innerHTML = "";
imageDiv?.appendChild(imgCloned);
this._modal.show();
});
});
};
}
customElements.define("custom-carousel", CustomCarousel);

View File

@@ -0,0 +1,7 @@
---
---
<iframe
src={Astro.props.pdf}
class={"w-9/10 md:w-3/5 h-7/10 md:h-4/5 " + Astro.props.class || ""}></iframe>

View File

@@ -0,0 +1,11 @@
---
import { type videoConfig } from "@interfaces/video.ts";
const config: videoConfig = Astro.props.videoConfig;
console.log(config);
---
<video class="h-auto w-full max-w-1/2" controls>
<source src={config.videoPath} type={config.videoType ?? "video/mp4"} />
Your browser does not support the video tag.
</video>

View File

@@ -0,0 +1,13 @@
---
import type { videoConfig } from "@interfaces/video.ts";
const config: videoConfig = Astro.props.videoConfig;
---
<iframe
class="h-128 w-full max-w-1/2"
src={config.videoPath}
title={config.videoTitle ?? ""}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen></iframe>

View File

@@ -2,31 +2,47 @@
import NestedNavbarEntry from "@components/NestedNavbarEntries.astro"; import NestedNavbarEntry from "@components/NestedNavbarEntries.astro";
import logo_title_large from "@assets/logo-title-large.png"; import logo_title_large from "@assets/logo-title-large.png";
import {siteLayout} from "@data/site-layout.ts"; import { siteLayout } from "@data/site-layout.ts";
import {Image} from "astro:assets"; import { Image } from "astro:assets";
--- ---
<nav class="border-b-caperren-green text-caperren-green border-b-4">
<nav class="border-b-4 border-b-caperren-green text-caperren-green"> <div class="mx-auto flex flex-wrap items-center justify-between p-6">
<div class="flex flex-wrap items-center justify-between mx-auto p-6"> <a href="/">
<a href="/"> <Image
<Image src={logo_title_large} src={logo_title_large}
class="h-10 lg:h-14 w-auto" class="h-10 w-auto lg:h-14"
alt="logo title" alt="logo title"
loading="eager" loading="eager"
/> />
</a> </a>
<button data-collapse-toggle="navbar-multi-level" type="button" <button
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm lg:hidden focus:ring-2 focus:ring-caperren-green" data-collapse-toggle="navbar-multi-level"
aria-controls="navbar-multi-level" aria-expanded="false"> type="button"
<span class="sr-only">Open main menu</span> class="focus:ring-caperren-green inline-flex h-10 w-10 items-center justify-center p-2 text-sm focus:ring-2 lg:hidden"
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17 14"> aria-controls="navbar-multi-level"
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-expanded="false"
d="M1 1h15M1 7h15M1 13h15"/> >
</svg> <span class="sr-only">Open main menu</span>
</button> <svg
<div class="z-40 hidden mt-1 w-full lg:block lg:w-auto" id="navbar-multi-level"> class="h-5 w-5"
<NestedNavbarEntry items={siteLayout}/> aria-hidden="true"
</div> xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 17 14"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M1 1h15M1 7h15M1 13h15"></path>
</svg>
</button>
<div
class="z-40 mt-1 hidden w-full lg:block lg:w-auto"
id="navbar-multi-level"
>
<NestedNavbarEntry items={siteLayout} />
</div> </div>
</div>
</nav> </nav>

View File

@@ -1,51 +1,78 @@
--- ---
import type {navLink} from "@interfaces/site-layout.ts"; import type { navLink } from "@interfaces/site-layout.ts";
const items: navLink[] = Astro.props.items; const items: navLink[] = Astro.props.items;
const depth: number = Astro.props.depth ?? 0; const depth: number = Astro.props.depth ?? 0;
const paths: string[] = Astro.props.paths ?? []; const paths: string[] = Astro.props.paths ?? [];
const getNavLinkSuffix = (entry: navLink): string => { const getNavLinkSuffix = (entry: navLink): string => {
return "-" + [...paths, entry.path].join("-") return "-" + [...paths, entry.path].join("-");
} };
const getHrefPath = (entry: navLink): string => { const getHrefPath = (entry: navLink): string => {
return entry.pubpath ? entry.pubpath : ("/" + (paths && paths.length ? [...paths, entry.path].join("/") : entry.path)); return entry.pubpath
} ? entry.pubpath
: "/" +
(paths && paths.length ? [...paths, entry.path].join("/") : entry.path);
};
--- ---
<ul class={"flex flex-col p-4 bg-black border-caperren-green " + (depth ? "space-y-2" : "items-start lg:flex-row lg:space-x-8 lg:mt-0 ")}>
{
items.map((entry) => (
(entry.enabled ?? true) && (
<li>
{Array.isArray(entry.children) && entry.children.length ? (
<button id={"dropdownNavbarLink" + getNavLinkSuffix(entry)}
data-dropdown-toggle={"dropdownNavbar" + getNavLinkSuffix(entry)}
data-dropdown-placement="bottom"
class="flex items-center justify-between py-2 px-3 w-full hover:text-caperren-green-light lg:hover:bg-transparent lg:border-0 lg:hover:text-caperren-green-light lg:p-0 ">
{entry.navText}
<svg class="w-2.5 h-2.5 ms-2.5" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="m1 1 4 4 4-4"/>
</svg>
</button>
<div id={"dropdownNavbar" + getNavLinkSuffix(entry)} <ul
class="z-10 hidden bg-black border border-caperren-green shadow-sm w-screen lg:w-max"> class={"flex flex-col p-4 bg-black border-caperren-green " +
<Astro.self items={entry.children} paths={[...paths, entry.path]} (depth ? "space-y-2" : "items-start lg:flex-row lg:space-x-8 lg:mt-0 ")}
depth={depth + 1}/> >
</div> {
items.map(
) : ( (entry) =>
(entry.enabled ?? true) && (
<a href={getHrefPath(entry)} <li>
class="block py-2 px-3 bg-transparent hover:text-caperren-green-light ring-caperren-green-dark lg:p-0" {Array.isArray(entry.children) && entry.children.length ? (
aria-current="page">{entry.navText}</a> <div>
<button
)} id={"dropdownNavbarLink" + getNavLinkSuffix(entry)}
</li> data-dropdown-toggle={
) "dropdownNavbar" + getNavLinkSuffix(entry)
))} }
</ul> data-dropdown-placement="bottom"
class="hover:text-caperren-green-light lg:hover:text-caperren-green-light flex w-full items-center justify-between px-3 py-2 lg:border-0 lg:p-0 lg:hover:bg-transparent"
>
{entry.navText}
<svg
class="ms-2.5 h-2.5 w-2.5"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 10 6"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m1 1 4 4 4-4"
/>
</svg>
</button>
<div
id={"dropdownNavbar" + getNavLinkSuffix(entry)}
class="border-caperren-green z-10 hidden w-screen border bg-black shadow-sm lg:w-max"
>
<Astro.self
items={entry.children}
paths={[...paths, entry.path]}
depth={depth + 1}
/>
</div>
</div>
) : (
<a
href={getHrefPath(entry)}
class="hover:text-caperren-green-light ring-caperren-green-dark block bg-transparent px-3 py-2 lg:p-0"
aria-current="page"
>
{entry.navText}
</a>
)}
</li>
),
)
}
</ul>

View File

@@ -1,5 +1,5 @@
--- ---
import type {tableData} from "@interfaces/table.ts"; import type { tableData } from "@interfaces/table.ts";
const data: tableData = Astro.props.data; const data: tableData = Astro.props.data;
const columnPadding: number = data.columnPadding || 2; const columnPadding: number = data.columnPadding || 2;
@@ -8,27 +8,33 @@ const paddingClasses: string = `px-${columnPadding} py-${rowPadding}`;
--- ---
<div class="relative max-w-full overflow-x-auto"> <div class="relative max-w-full overflow-x-auto">
<table class="text-sm text-left"> <table class="text-left text-sm">
<thead class="text-xs border-b-3 border-caperren-green uppercase bg-black"> <thead class="border-caperren-green border-b-3 bg-black text-xs uppercase">
<tr> <tr>
{data.header.map(headingText => ( {
<th scope="col" class={paddingClasses}> data.header.map((headingText) => (
{headingText} <th scope="col" class={paddingClasses}>
</th> {headingText}
</th>
))
}
</tr>
</thead>
<tbody>
{
data.rows.map((row) => (
<tr class="border-caperren-green border-b dark:bg-black">
{row.map((rowColumnText) => (
<th
scope="row"
class={paddingClasses + " font-medium whitespace-nowrap"}
>
{rowColumnText}
</th>
))} ))}
</tr> </tr>
</thead> ))
<tbody> }
{data.rows.map(row => ( </tbody>
<tr class=" border-b dark:bg-black border-caperren-green"> </table>
{row.map(rowColumnText => ( </div>
<th scope="row"
class={paddingClasses + " font-medium whitespace-nowrap"}>
{rowColumnText}
</th>
))}
</tr>
))}
</tbody>
</table>
</div>

View File

@@ -1,36 +1,39 @@
--- ---
import type {timelineEntry} from "@interfaces/timeline.ts"; import type { timelineEntry } from "@interfaces/timeline.ts";
const timeline: timelineEntry[] = Astro.props.timeline || []; const timeline: timelineEntry[] = Astro.props.timeline || [];
--- ---
<custom-timeline> <custom-timeline>
<div class="flex flex-col w-full"> <div class="flex w-full flex-col">
<div class="z-10 grid gap-6 grid-flow-row max-w-full sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 3xl:grid-cols-6" <div
data-timeline> class="3xl:grid-cols-6 z-10 grid max-w-full grid-flow-row gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
{timeline.map((entry, index) => ( data-timeline
<div class="pt-1 border bg-black border-caperren-green rounded-lg min-w-s max-w-s px-2 pb-2" >
data-timeline-node-index={index} {
> timeline.map((entry, index) => (
<h3 class="text-lg font-bold"> <div
{entry.event} class="border-caperren-green min-w-s max-w-s rounded-lg border bg-black px-2 pt-1 pb-2"
</h3> data-timeline-node-index={index}
<h4 class="font-semibold leading-none"> >
{entry.eventDetail} <h3 class="text-lg font-bold">{entry.event}</h3>
</h4> <h4 class="leading-none font-semibold">{entry.eventDetail}</h4>
<time class="mb-2 mt-1 text-sm italic leading-none"> <time class="mt-1 mb-2 text-sm leading-none italic">
{entry.date} {entry.date}
</time> </time>
{entry.description && ( {entry.description && (
<p class="text-sm font-normal"> <p class="text-sm font-normal">{entry.description}</p>
{entry.description} )}
</p> </div>
)} ))
</div> }
))}
</div>
<div class="z-0 w-full max-w-full overflow-x-visible" data-custom-timeline-line-wrapper/>
</div> </div>
<div
class="z-0 w-full max-w-full overflow-x-visible"
data-custom-timeline-line-wrapper
>
</div>
</div>
</custom-timeline> </custom-timeline>
<script src="./timeline.ts"/> <script src="./timeline.ts"></script>

View File

@@ -1,70 +1,80 @@
import LeaderLine from "leader-line-new"; import LeaderLine from "leader-line-new";
class CustomTimeline extends HTMLElement { class CustomTimeline extends HTMLElement {
_eventElements: Element[]; _eventElements: Element[];
constructor() { constructor() {
super(); super();
this._eventElements = this._getNodeElements(); this._eventElements = this._getNodeElements();
window.addEventListener("load", this._paintLeaderLines); window.addEventListener("load", this._paintLeaderLines);
} }
_getNodeElements = (): Element[] => _getNodeElements = (): Element[] =>
Array.from(this.querySelectorAll('[data-timeline-node-index]')).sort( Array.from(this.querySelectorAll("[data-timeline-node-index]")).sort(
(elementA, elementB) => (elementA, elementB) =>
Number(elementA.getAttribute('data-timeline-node-index')) - Number(elementB.getAttribute('data-timeline-node-index')) Number(elementA.getAttribute("data-timeline-node-index")) -
); Number(elementB.getAttribute("data-timeline-node-index")),
);
_paintLeaderLines = () => {
let pairs = this._eventElements.map((entry, index) => {
if (index < this._eventElements.length - 1) {
return [entry, this._eventElements[index + 1]];
}
}).filter(pair => pair !== undefined)
let contentBodyScrolling = document.getElementById("content-body-scrolling");
let wrapper = this.querySelector("[data-custom-timeline-line-wrapper]") as HTMLElement;
const position = (line: LeaderLine) => {
wrapper.style.transform = "none";
let rectWrapper = wrapper.getBoundingClientRect();
wrapper.style.transform = "translate(-" +
(Number(rectWrapper.left)) + "px, -"
+
(Number(rectWrapper.top)) + "px)"
line.position()
_paintLeaderLines = () => {
let pairs = this._eventElements
.map((entry, index) => {
if (index < this._eventElements.length - 1) {
return [entry, this._eventElements[index + 1]];
} }
})
.filter((pair) => pair !== undefined);
pairs.forEach(pair => { let contentBodyScrolling = document.getElementById(
let line = new LeaderLine({ "content-body-scrolling",
start: pair[0], );
end: pair[1], let wrapper = this.querySelector(
color: '#10ac25', "[data-custom-timeline-line-wrapper]",
size: 3, ) as HTMLElement;
startSocket: "right",
endSocket: "left",
startPlug: "square",
endPlug: "arrow3"
});
// Find new element by hidden id and add to wrapper div const position = (line: LeaderLine) => {
const newLineEl = document.querySelector(`body .leader-line:has(path#leader-line-${line._id}-line-path)`); wrapper.style.transform = "none";
if (newLineEl) wrapper.appendChild(newLineEl); let rectWrapper = wrapper.getBoundingClientRect();
// Add position updaters for scrolling and resize events wrapper.style.transform =
contentBodyScrolling?.addEventListener("scroll", () => position(line)); "translate(-" +
window.addEventListener("resize", () => position(line)); Number(rectWrapper.left) +
"px, -" +
Number(rectWrapper.top) +
"px)";
// Perform the initial positioning line.position();
position(line); };
});
} pairs.forEach((pair) => {
let line = new LeaderLine({
start: pair[0],
end: pair[1],
color: "#10ac25",
size: 3,
startSocket: "right",
endSocket: "left",
startPlug: "square",
endPlug: "arrow3",
});
// Find new element by hidden id and add to wrapper div
// noinspection TypeScriptUnresolvedReference
const lineId = line._id; // @ts-ignore
const newLineEl = document.querySelector(
`body .leader-line:has(path#leader-line-${lineId}-line-path)`,
);
if (newLineEl) wrapper.appendChild(newLineEl);
// Add position updaters for scrolling and resize events
contentBodyScrolling?.addEventListener("scroll", () => position(line));
window.addEventListener("resize", () => position(line));
// Perform the initial positioning
position(line);
});
};
} }
customElements.define('custom-timeline', CustomTimeline) customElements.define("custom-timeline", CustomTimeline);

View File

@@ -1,12 +0,0 @@
---
import {type videoConfig} from "@interfaces/video.ts";
const config: videoConfig = Astro.props.videoConfig;
console.log(config);
---
<video class="w-full h-auto max-w-1/2" controls>
<source src={config.videoPath} type={config.videoType ?? "video/mp4"}/>
Your browser does not support the video tag.
</video>

View File

@@ -1,9 +0,0 @@
---
import type {videoConfig} from "@interfaces/video.ts";
const config: videoConfig = Astro.props.videoConfig;
---
<iframe class="h-128 w-full max-w-1/2" src={config.videoPath}
title={config.videoTitle ?? ""}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

View File

@@ -1,223 +1,270 @@
import type {navLink} from "@interfaces/site-layout.ts" import type { navLink } from "@interfaces/site-layout.ts";
export const siteLayout: navLink[] = [ export const siteLayout: navLink[] = [
{navText: "About", path: ""}, { navText: "About", path: "" },
{navText: "Education", path: "education"}, { navText: "Education", path: "education" },
{ {
navText: "Experiences", navText: "Experiences",
path: "experience", path: "experience",
children: [
{
navText: "SpaceX",
path: "spacex",
children: [ children: [
{ {
navText: "SpaceX", navText: "Hardware Test Engineer I/II",
path: "spacex", path: "hardware-test-engineer-i-ii",
children: [ },
{ {
navText: "Hardware Test Engineer I/II", navText: "Avionics Test Engineering Internship",
path: "hardware-test-engineer-i-ii" path: "avionics-test-engineering-internship",
}, },
{ ],
navText: "Avionics Test Engineering Internship", },
path: "avionics-test-engineering-internship" {
} navText: "OSU CEOAS Ocean Mixing Group",
] path: "osu-ceoas-ocean-mixing-group",
},
{
enabled: false,
navText: "OSU CEOAS",
path: "osu-ceoas-ocean-mixing-group",
children: [
{
enabled: false,
navText: "Robotics Oceanographic Surface Sampler",
path: "robotic-oceanographic-surface-sampler",
},
{
enabled: false,
navText: "LeConte Glacier Deployments",
path: "leconte-glacier-deployments",
}
]
},
{
enabled: false,
navText: "OSU SARL",
path: "osu-sinnhuber-aquatic-research-laboratory",
children: [
{
enabled: false,
navText: "Team Lead",
path: "team-lead",
},
{
enabled: false,
navText: "Zebrafish Embryo Pick and Plate",
path: "zebrafish-embryo-pick-and-plate",
},
{
enabled: false,
navText: "Shuttlebox Behavior System",
path: "shuttlebox-behavior-system",
},
{
enabled: false,
navText: "Dechorionator",
path: "dechorionator",
},
{
enabled: false,
navText: "Denso Embryo Pick and Plate",
path: "denso-embryo-pick-and-plate",
},
{
enabled: false,
navText: "ZScan Processor",
path: "zscan-processor",
}
]
},
{
enabled: false,
navText: "OSU Robotics Club",
path: "osu-robotics-club",
children: [
{
enabled: false,
navText: "Mars Rover Software Team Lead",
path: "mars-rover-software-team-lead",
},
{
enabled: false,
navText: "Mars Rover Emergency Software Team Lead",
path: "mars-rover-emergency-software-team-lead",
},
{
enabled: false,
navText: "Mars Rover Electrical Team Lead",
path: "mars-rover-electrical-team-lead",
},
{
enabled: false,
navText: "Club Officer",
path: "club-officer",
}
]
},
]
},
{
navText: "Hobbies",
path: "hobby",
children: [ children: [
{
{ navText: "Robotics Oceanographic Surface Sampler",
enabled: false, path: "robotic-oceanographic-surface-sampler",
navText: "Homelab", path: "homelab", },
children: [ {
{enabled: false, navText: "Home Server Rack", path: "home-server-rack"}, enabled: false,
{enabled: false, navText: "Offsite Backup Rack", path: "offsite-backup-rack"}, navText: "LeConte Glacier Deployments",
{enabled: false, navText: "Kubernetes Cluster", path: "kubernetes-cluster"}, path: "leconte-glacier-deployments",
{enabled: false, navText: "Home Automation", path: "home-automation"}, },
] ],
}, },
{ {
navText: "Motorcycling", enabled: false,
path: "motorcycling", navText: "OSU SARL",
children: [ path: "osu-sinnhuber-aquatic-research-laboratory",
{navText: "Lineup", path: "lineup"},
{
navText: "Custom Accessories",
path: "custom-accessories",
children: [
{navText: "Chubby Buttons 2 Mount", path: "chubby-buttons-2-mount"},
]
},
{
enabled: false,
navText: "Trips",
path: "trips",
children: [
{navText: "2025-08 | Alaska ", path: "2025-08-alaska", enabled: false,},
{navText: "2024-10 | Norway ", path: "2024-10-norway", enabled: false,}
]
},
]
},
{
enabled: false,
navText: "Projects",
path: "projects",
children: [
{navText: "OSSM Overkill Edition", path: "ossm-overkill-edition", enabled: false},
{navText: "Rachael Ray Light Box", path: "rachael-ray-light-box", enabled: false},
{navText: "Shed Solar", path: "shed-solar", enabled: false},
]
},
{enabled: false, navText: "NixOS", path: "nixos"},
{navText: "Body Mods", path: "body-mods"},
]
},
{
navText: "Resumes",
path: "resume",
children: [ children: [
{enabled: false, navText: "2025-11-10 | Complete CV", path: "2025-11-10-complete-cv"}, {
{navText: "2025-11-10 | Infrastructure Engineer", path: "2025-11-10-infrastructure-engineer"}, enabled: false,
{navText: "2019-07-01 | Hardware Test Engineer", path: "2019-07-01-hardware-test-engineer"}, navText: "Team Lead",
] path: "team-lead",
}, },
{navText: "Github", pubpath: "https://github.com/caperren"}, {
{navText: "LinkedIn", pubpath: "https://www.linkedin.com/in/caperren/"} enabled: false,
] navText: "Zebrafish Embryo Pick and Plate",
path: "zebrafish-embryo-pick-and-plate",
},
{
enabled: false,
navText: "Shuttlebox Behavior System",
path: "shuttlebox-behavior-system",
},
{
enabled: false,
navText: "Dechorionator",
path: "dechorionator",
},
{
enabled: false,
navText: "Denso Embryo Pick and Plate",
path: "denso-embryo-pick-and-plate",
},
{
enabled: false,
navText: "ZScan Processor",
path: "zscan-processor",
},
],
},
{
enabled: false,
navText: "OSU Robotics Club",
path: "osu-robotics-club",
children: [
{
enabled: false,
navText: "Mars Rover Software Team Lead",
path: "mars-rover-software-team-lead",
},
{
enabled: false,
navText: "Mars Rover Emergency Software Team Lead",
path: "mars-rover-emergency-software-team-lead",
},
{
enabled: false,
navText: "Mars Rover Electrical Team Lead",
path: "mars-rover-electrical-team-lead",
},
{
enabled: false,
navText: "Club Officer",
path: "club-officer",
},
],
},
],
},
{
navText: "Hobbies",
path: "hobby",
children: [
{
enabled: false,
navText: "Homelab",
path: "homelab",
children: [
{
enabled: false,
navText: "Home Server Rack",
path: "home-server-rack",
},
{
enabled: false,
navText: "Offsite Backup Rack",
path: "offsite-backup-rack",
},
{
enabled: false,
navText: "Kubernetes Cluster",
path: "kubernetes-cluster",
},
{
enabled: false,
navText: "Home Automation",
path: "home-automation",
},
],
},
{
navText: "Motorcycling",
path: "motorcycling",
children: [
{ navText: "Lineup", path: "lineup" },
{
navText: "Custom Accessories",
path: "custom-accessories",
children: [
{
navText: "Chubby Buttons 2 Mount",
path: "chubby-buttons-2-mount",
},
],
},
{
enabled: false,
navText: "Trips",
path: "trips",
children: [
{
navText: "2025-08 | Alaska ",
path: "2025-08-alaska",
enabled: false,
},
{
navText: "2024-10 | Norway ",
path: "2024-10-norway",
enabled: false,
},
],
},
],
},
{
enabled: false,
navText: "Projects",
path: "projects",
children: [
{
navText: "OSSM Overkill Edition",
path: "ossm-overkill-edition",
enabled: false,
},
{
navText: "Rachael Ray Light Box",
path: "rachael-ray-light-box",
enabled: false,
},
{ navText: "Shed Solar", path: "shed-solar", enabled: false },
],
},
{ enabled: false, navText: "NixOS", path: "nixos" },
{ navText: "Body Mods", path: "body-mods" },
],
},
{
navText: "Resumes",
path: "resume",
children: [
{
enabled: false,
navText: "2025-11-10 | Complete CV",
path: "2025-11-10-complete-cv",
},
{
navText: "2025-11-10 | Infrastructure Engineer",
path: "2025-11-10-infrastructure-engineer",
},
{
navText: "2019-07-01 | Hardware Test Engineer",
path: "2019-07-01-hardware-test-engineer",
},
],
},
{ navText: "Github", pubpath: "https://github.com/caperren" },
{ navText: "LinkedIn", pubpath: "https://www.linkedin.com/in/caperren/" },
];
export const pathToMetadata = (path: string): navLink => { export const pathToMetadata = (path: string): navLink => {
let paths = path.split("/").filter((entry) => entry); let paths = path.split("/").filter((entry) => entry);
// Handle root path of / // Handle root path of /
if (paths.length < 1) { if (paths.length < 1) {
paths = [""] paths = [""];
} }
let currentEntries: navLink[] = siteLayout; let currentEntries: navLink[] = siteLayout;
let foundEntry: navLink | undefined; let foundEntry: navLink | undefined;
for (const path of paths) { for (const path of paths) {
for (const currentEntry of currentEntries) { for (const currentEntry of currentEntries) {
if (currentEntry.path === path) { if (currentEntry.path === path) {
foundEntry = currentEntry; foundEntry = currentEntry;
if (foundEntry.children && foundEntry.children.length > 0) { if (foundEntry.children && foundEntry.children.length > 0) {
currentEntries = foundEntry.children; currentEntries = foundEntry.children;
}
}
} }
}
} }
}
if (foundEntry === undefined) { if (foundEntry === undefined) {
throw new Error(`${path} not found in site layout!`); throw new Error(`${path} not found in site layout!`);
} }
return foundEntry; return foundEntry;
} };
export const getPaths = ( export const getPaths = (
currentEntries: navLink[] = siteLayout, currentEntries: navLink[] = siteLayout,
paths: string[] = [], paths: string[] = [],
disabledOnly = false disabledOnly = false,
): string[] => { ): string[] => {
let foundPaths: string[] = []; let foundPaths: string[] = [];
for (const currentEntry of currentEntries) { for (const currentEntry of currentEntries) {
if (currentEntry.children && currentEntry.children.length > 0) { if (currentEntry.children && currentEntry.children.length > 0) {
foundPaths = [ foundPaths = [
...foundPaths, ...foundPaths,
...getPaths(currentEntry.children, [...paths, currentEntry.path || ""], disabledOnly) ...getPaths(
] currentEntry.children,
} else { [...paths, currentEntry.path || ""],
let enabled = currentEntry.enabled ?? true; disabledOnly,
if (disabledOnly ? !enabled : enabled) { ),
foundPaths.push("/" + [...paths, currentEntry.path || ""].join("/")); ];
} } else {
} let enabled = currentEntry.enabled ?? true;
if (disabledOnly ? !enabled : enabled) {
foundPaths.push("/" + [...paths, currentEntry.path || ""].join("/"));
}
} }
return [...new Set(foundPaths)]; }
} return [...new Set(foundPaths)];
};

8
src/env.d.ts vendored
View File

@@ -1,8 +1,8 @@
interface ImportMetaEnv { interface ImportMetaEnv {
readonly PUBLIC_REPO_VERSION_HASH: string; readonly PUBLIC_REPO_VERSION_HASH: string;
readonly PUBLIC_BUILD_ENVIRONMENT: string; readonly PUBLIC_BUILD_ENVIRONMENT: string;
} }
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv; readonly env: ImportMetaEnv;
} }

View File

@@ -1,14 +1,11 @@
import {DateTime} from 'luxon'; import { DateTime } from "luxon";
export interface experience { export interface experience {}
}
export interface subExperience { export interface subExperience {
name: string; name: string;
description: string; description: string;
startDate: DateTime; startDate: DateTime;
endDate?: DateTime; endDate?: DateTime;
} }

View File

@@ -1,5 +1,5 @@
export interface carouselGroup { export interface carouselGroup {
animation: "static" | "slide"; animation: "static" | "slide";
interval?: number; interval?: number;
images: ImageMetadata[]; images: ImageMetadata[];
} }

View File

@@ -1,7 +1,7 @@
export interface navLink { export interface navLink {
enabled?: boolean; enabled?: boolean;
navText: string; navText: string;
path?: string; path?: string;
pubpath?: string; pubpath?: string;
children?: navLink[]; children?: navLink[];
} }

View File

@@ -1,6 +1,6 @@
export interface tableData { export interface tableData {
header: string[]; header: string[];
columnPadding?: number; columnPadding?: number;
rowPadding?: number; rowPadding?: number;
rows: Array<Array<any>>; rows: Array<Array<any>>;
} }

View File

@@ -1,6 +1,6 @@
export interface timelineEntry { export interface timelineEntry {
event: string; event: string;
eventDetail?: string eventDetail?: string;
date: string; date: string;
description?: string; description?: string;
} }

View File

@@ -1,5 +1,5 @@
export interface videoConfig { export interface videoConfig {
videoTitle?: string videoTitle?: string;
videoPath: string; videoPath: string;
videoType?: string; videoType?: string;
} }

View File

@@ -1,46 +1,53 @@
--- ---
import '@styles/global.css' import "@styles/global.css";
import Navbar from '@components/Navbar.astro'; import Navbar from "@components/Navbar.astro";
import Footer from '@components/Footer.astro'; import Footer from "@components/Footer.astro";
import {pathToMetadata} from "@data/site-layout.ts"; import { pathToMetadata } from "@data/site-layout.ts";
const pageTitle = Astro.props.title
const pageTitle = Astro.props.title ? `${Astro.props.title} - Corwin Perren` : "Corwin Perren"; ? `${Astro.props.title} - Corwin Perren`
: "Corwin Perren";
const showTitle = Astro.props.showTitle ?? true; const showTitle = Astro.props.showTitle ?? true;
const pageEnabled = pathToMetadata(Astro.url.pathname).enabled ?? true; const pageEnabled = pathToMetadata(Astro.url.pathname).enabled ?? true;
--- ---
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8" />
<link rel="icon" href="/48x48-favicon.png" type="image/x-icon"/> <link rel="icon" href="/48x48-favicon.png" type="image/x-icon" />
<link rel="icon" href="/512x512-favicon.png" type="image/png"/> <link rel="icon" href="/512x512-favicon.png" type="image/png" />
<link rel="icon" href="/192x192-favicon.png" type="image/png"/> <link rel="icon" href="/192x192-favicon.png" type="image/png" />
<link rel="icon" href="/180x180-favicon.png" type="image/png"/> <link rel="icon" href="/180x180-favicon.png" type="image/png" />
<link rel="icon" href="/48x48-favicon.png" type="image/png"/> <link rel="icon" href="/48x48-favicon.png" type="image/png" />
<link rel="icon" href="/32x32-favicon.png" type="image/png"/> <link rel="icon" href="/32x32-favicon.png" type="image/png" />
<link rel="icon" href="/16x16-favicon.png" type="image/png"/> <link rel="icon" href="/16x16-favicon.png" type="image/png" />
<link rel="sitemap" href="/sitemap-index.xml"/> <link rel="sitemap" href="/sitemap-index.xml" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{pageEnabled ? pageTitle : "Corwin Perren"}</title> <title>{pageEnabled ? pageTitle : "Corwin Perren"}</title>
</head> </head>
<body class="flex flex-col bg-black text-white h-dvh w-full max-w-full"> <body class="flex h-dvh w-full max-w-full flex-col bg-black text-white">
<div id="content-body-scrolling" class="grow overflow-x-hidden overflow-y-scroll"> <div
<Navbar/> id="content-body-scrolling"
<main class="mx-6 my-6"> class="grow overflow-x-hidden overflow-y-scroll"
{(Astro.props.title && showTitle && pageEnabled) && ( >
<h1 class="font-extrabold md:text-3xl md:mb-6">{Astro.props.title}</h1> <Navbar />
)} <main class="mx-6 my-6">
{pageEnabled && ( {
<slot/> Astro.props.title && showTitle && pageEnabled && (
)} <h1 class="font-extrabold md:mb-6 md:text-3xl">
</main> {Astro.props.title}
</div> </h1>
<Footer/> )
<script src="../scripts/main.ts"></script> }
</body> {pageEnabled && <slot />}
</html> </main>
</div>
<Footer />
<script src="../scripts/main.ts"></script>
</body>
</html>

View File

@@ -1,6 +1,7 @@
--- ---
import BaseLayout from './BaseLayout.astro'; import BaseLayout from "./BaseLayout.astro";
--- ---
<BaseLayout title={Astro.props.title}> <BaseLayout title={Astro.props.title}>
<slot/> <slot />
</BaseLayout> </BaseLayout>

View File

@@ -1,6 +1,7 @@
--- ---
import BaseLayout from './BaseLayout.astro'; import BaseLayout from "./BaseLayout.astro";
--- ---
<BaseLayout title={Astro.props.title}> <BaseLayout title={Astro.props.title}>
<slot/> <slot />
</BaseLayout> </BaseLayout>

View File

@@ -1,10 +1,10 @@
--- ---
import BaseLayout from './BaseLayout.astro'; import BaseLayout from "@layouts/BaseLayout.astro";
import PdfViewer from "@components/Media/PdfViewer.astro";
---
const resume = Astro.props.resume;
---
<BaseLayout title={Astro.props.title}> <BaseLayout title={Astro.props.title}>
<div class="h-dvh"> <div class="h-dvh">
<iframe src={resume} class="mx-auto w-9/10 md:w-3/5 h-7/10 md:h-4/5"/> <PdfViewer class="mx-auto" pdf={Astro.props.resume} />
</div> </div>
</BaseLayout> </BaseLayout>

View File

@@ -1,117 +1,117 @@
--- ---
import BaseLayout from "@layouts/BaseLayout.astro"; import BaseLayout from "@layouts/BaseLayout.astro";
import Carousel from "@components/CustomCarousel/CustomCarousel.astro"; import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import Timeline from "@components/Timeline/Timeline.astro"; import Timeline from "@components/Timeline/Timeline.astro";
import Table from "@components/Table.astro"; import Table from "@components/Table.astro";
import type {carouselGroup} from "@interfaces/image-carousel.ts"; import type { carouselGroup } from "@interfaces/image-carousel.ts";
import type {tableData} from "@interfaces/table.ts"; import type { tableData } from "@interfaces/table.ts";
import type {timelineEntry} from "@interfaces/timeline.ts"; import type { timelineEntry } from "@interfaces/timeline.ts";
import fhhs_diploma from "@assets/education/fhhs-diploma.jpg"; import fhhs_diploma from "@assets/education/fhhs-diploma.jpg";
import osu_bs_cs_diploma from "@assets/education/osu-bs-cs-diploma.jpg" import osu_bs_cs_diploma from "@assets/education/osu-bs-cs-diploma.jpg";
const diplomaCarouselGroup: carouselGroup = { const diplomaCarouselGroup: carouselGroup = {
animation: "slide", animation: "slide",
images: [ images: [fhhs_diploma, osu_bs_cs_diploma],
fhhs_diploma, };
osu_bs_cs_diploma
]
}
const timeline: timelineEntry[] = [ const timeline: timelineEntry[] = [
{ {
event: "High School Diploma", event: "High School Diploma",
eventDetail: "Friday Harbor High School", eventDetail: "Friday Harbor High School",
date: "June 2011" date: "June 2011",
}, },
{ {
event: "B.S. Computer Science", event: "B.S. Computer Science",
eventDetail: "Oregon State University", eventDetail: "Oregon State University",
date: "June 2019" date: "June 2019",
}, },
]; ];
const courseTable: tableData = { const courseTable: tableData = {
header: ["Program", "Course", "Description"], header: ["Program", "Course", "Description"],
rows: [ rows: [
["CS", "LDT", "INTRO TO LINUX"], ["CS", "LDT", "INTRO TO LINUX"],
["CS", "261", "DATA STRUCTURES"], ["CS", "261", "DATA STRUCTURES"],
["CS", "271", "COMPUTER ARCH & ASSEM LANGUAGE"], ["CS", "271", "COMPUTER ARCH & ASSEM LANGUAGE"],
["CS", "290", "WEB DEVELOPMENT"], ["CS", "290", "WEB DEVELOPMENT"],
["CS", "312", "SYSTEM ADMINISTRATION"], ["CS", "312", "SYSTEM ADMINISTRATION"],
["CS", "325", "ANALYSIS OF ALGORITHMS"], ["CS", "325", "ANALYSIS OF ALGORITHMS"],
["CS", "331", "INTRO ARTIFICIAL INTELLIGENCE"], ["CS", "331", "INTRO ARTIFICIAL INTELLIGENCE"],
["CS", "340", "INTRODUCTION TO DATABASES"], ["CS", "340", "INTRODUCTION TO DATABASES"],
["CS", "344", "OPERATING SYSTEMS I"], ["CS", "344", "OPERATING SYSTEMS I"],
["CS", "352", "INTRO TO USABILITY ENGINEERING"], ["CS", "352", "INTRO TO USABILITY ENGINEERING"],
["CS", "361", "SOFTWARE ENGINEERING I"], ["CS", "361", "SOFTWARE ENGINEERING I"],
["CS", "362", "SOFTWARE ENGINEERING II"], ["CS", "362", "SOFTWARE ENGINEERING II"],
["CS", "370", "INTRODUCTION TO SECURITY"], ["CS", "370", "INTRODUCTION TO SECURITY"],
["CS", "372", "INTRO TO COMPUTER NETWORKS"], ["CS", "372", "INTRO TO COMPUTER NETWORKS"],
["CS", "381", "PROGRAMMING LANGUAGE FUND"], ["CS", "381", "PROGRAMMING LANGUAGE FUND"],
["CS", "391", "SOC & ETHICAL ISSUES IN COMSC"], ["CS", "391", "SOC & ETHICAL ISSUES IN COMSC"],
["CS", "444", "OPERATING SYSTEMS II"], ["CS", "444", "OPERATING SYSTEMS II"],
["CS", "461", "SENIOR SOFTWARE ENGR PROJECT I"], ["CS", "461", "SENIOR SOFTWARE ENGR PROJECT I"],
["CS", "462", "SENIOR SOFTWARE ENGR PROJECT II"], ["CS", "462", "SENIOR SOFTWARE ENGR PROJECT II"],
["CS", "463", "SENIOR SOFTWARE ENGR PROJECT III"], ["CS", "463", "SENIOR SOFTWARE ENGR PROJECT III"],
["CS", "464", "OPEN SOURCE SOFTWARE"], ["CS", "464", "OPEN SOURCE SOFTWARE"],
["CS", "468", "INCLUSIVE DESIGN (HCI)"], ["CS", "468", "INCLUSIVE DESIGN (HCI)"],
["CS", "496", "MOBILE/CLOUD SOFTWARE DEVEL"], ["CS", "496", "MOBILE/CLOUD SOFTWARE DEVEL"],
["ECE", "111", "INTRODUCTION TO ECE: TOOLS"], ["ECE", "111", "INTRODUCTION TO ECE: TOOLS"],
["ECE", "112", "INTRODUCTION TO ECE: CONCEPTS"], ["ECE", "112", "INTRODUCTION TO ECE: CONCEPTS"],
["ECE", "151", "PROGRAMMING I/EMBED CONTR LAB"], ["ECE", "151", "PROGRAMMING I/EMBED CONTR LAB"],
["ECE", "152", "PROGRAMMING II/EMBED CONTR LAB"], ["ECE", "152", "PROGRAMMING II/EMBED CONTR LAB"],
["ECE", "271", "DIGITAL LOGIC DESIGN"], ["ECE", "271", "DIGITAL LOGIC DESIGN"],
["ECE", "272", "DIGITAL LOGIC DESIGN LAB"], ["ECE", "272", "DIGITAL LOGIC DESIGN LAB"],
["ECE", "375", "COMPUTER ORG & ASSEMBLY LANG"], ["ECE", "375", "COMPUTER ORG & ASSEMBLY LANG"],
["ENGR", "201", "ELECTRICAL FUNDAMENTALS I"], ["ENGR", "201", "ELECTRICAL FUNDAMENTALS I"],
["ENGR", "202", "ELECTRICAL FUNDAMENTALS II"], ["ENGR", "202", "ELECTRICAL FUNDAMENTALS II"],
["ENGR", "391", "ENGINEERING ECON & PROJ MGMT"], ["ENGR", "391", "ENGINEERING ECON & PROJ MGMT"],
["ROB", "421", "APPLIED ROBOTICS"], ["ROB", "421", "APPLIED ROBOTICS"],
["ROB", "456", "INTELLIGENT ROBOTS"], ["ROB", "456", "INTELLIGENT ROBOTS"],
["MTH", "231", "ELEMENTS DISCRETE MATH"], ["MTH", "231", "ELEMENTS DISCRETE MATH"],
["MTH", "241", "CALC FOR MGT & SOCIAL SCI"], ["MTH", "241", "CALC FOR MGT & SOCIAL SCI"],
["MTH", "251", "DIFFERENTIAL CALCULUS"], ["MTH", "251", "DIFFERENTIAL CALCULUS"],
["MTH", "252", "INTEGRAL CALCULUS"], ["MTH", "252", "INTEGRAL CALCULUS"],
["MTH", "254", "VECTOR CALCULUS I"], ["MTH", "254", "VECTOR CALCULUS I"],
["MTH", "306", "MATRIX & POWER SERIES METHODS"], ["MTH", "306", "MATRIX & POWER SERIES METHODS"],
["ST", "314", "INTRO TO STATS FOR ENGINEERS"], ["ST", "314", "INTRO TO STATS FOR ENGINEERS"],
["PH", "211", "GENERAL PHYSICS WITH CALCULUS I"], ["PH", "211", "GENERAL PHYSICS WITH CALCULUS I"],
["PH", "212", "GENERAL PHYSICS WITH CALCULUS II"], ["PH", "212", "GENERAL PHYSICS WITH CALCULUS II"],
["PH", "213", "GENERAL PHYSICS WITH CALCULUS III"], ["PH", "213", "GENERAL PHYSICS WITH CALCULUS III"],
["WR", "LDT", "COMPOSITION III"], ["WR", "LDT", "COMPOSITION III"],
["WR", "LDT", "CREATIVE WRITING"], ["WR", "LDT", "CREATIVE WRITING"],
["WR", "121", "ENGLISH COMPOSITION"], ["WR", "121", "ENGLISH COMPOSITION"],
["WR", "214", "WRITING IN BUSINESS"], ["WR", "214", "WRITING IN BUSINESS"],
["WR", "327", "TECHNICAL WRITING"], ["WR", "327", "TECHNICAL WRITING"],
["BI", "102", "GENERAL BIOLOGY"], ["BI", "102", "GENERAL BIOLOGY"],
["BI", "349", "BIODIVERSITY: CAUSES, CONSERV"], ["BI", "349", "BIODIVERSITY: CAUSES, CONSERV"],
["CH", "201", "CHEMISTRY FOR ENGINEERING MAJ"], ["CH", "201", "CHEMISTRY FOR ENGINEERING MAJ"],
["CH", "211", "RECITATION FOR CHEMISTRY 201"], ["CH", "211", "RECITATION FOR CHEMISTRY 201"],
["COMM", "114", "ARGUMENT & CRITICAL DISCOURSE"], ["COMM", "114", "ARGUMENT & CRITICAL DISCOURSE"],
["GEO", "LDT", "SURVEY EARTH SCIENCE"], ["GEO", "LDT", "SURVEY EARTH SCIENCE"],
["HDFS", "240", "HUMAN SEXUALITY"], ["HDFS", "240", "HUMAN SEXUALITY"],
["HHS", "231", "LIFETIME FITNESS FOR HEALTH"], ["HHS", "231", "LIFETIME FITNESS FOR HEALTH"],
["HHS", "246", "LIFETIME FITNESS: WALKING"], ["HHS", "246", "LIFETIME FITNESS: WALKING"],
["MUS", "LDT", "LA: MUSIC APPRECIATION"], ["MUS", "LDT", "LA: MUSIC APPRECIATION"],
["MUS", "108", "MUSIC CULTURES/ NAT AM FLUTE"], ["MUS", "108", "MUSIC CULTURES/ NAT AM FLUTE"],
["PAC", "123", "BOWLING I"], ["PAC", "123", "BOWLING I"],
["PHL", "205", "ETHICS"], ["PHL", "205", "ETHICS"],
["PS", "LDT", "AMERICAN GOVT"], ["PS", "LDT", "AMERICAN GOVT"],
["PSY", "201", "GENERAL PSYCHOLOGY"], ["PSY", "201", "GENERAL PSYCHOLOGY"],
["QS", "262", "INTRODUCTION TO QUEER STUDIES"] ["QS", "262", "INTRODUCTION TO QUEER STUDIES"],
] ],
}; };
--- ---
<BaseLayout title="Education"> <BaseLayout title="Education">
<Carousel carouselGroup={diplomaCarouselGroup}/> <Carousel carouselGroup={diplomaCarouselGroup} />
<h2 class="font-bold md:text-2xl my-4 underline">Timeline</h2> <h2 class="my-4 font-bold underline md:text-2xl">Timeline</h2>
<Timeline timeline={timeline}/> <Timeline timeline={timeline} />
<h2 class="font-bold md:text-2xl my-4 underline">Oregon State University</h2> <h2 class="my-4 font-bold underline md:text-2xl">Oregon State University</h2>
<a class="font-bold md:text-lg my-4 text-blue-500 underline hover:text-blue-300" <a
href="https://github.com/caperren/school_archives/tree/master/OSU%20Coursework">Coursework Archives</a> class="my-4 font-bold text-blue-500 underline hover:text-blue-300 md:text-lg"
<h3 class="font-bold md:text-lg my-4 underline">Course Listing</h3> href="https://github.com/caperren/school_archives/tree/master/OSU%20Coursework"
<Table data={courseTable}/> >Coursework Archives</a
</BaseLayout> >
<h3 class="my-4 font-bold underline md:text-lg">Course Listing</h3>
<Table data={courseTable} />
</BaseLayout>

View File

@@ -2,5 +2,4 @@
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
--- ---
<ExperienceLayout title="CEOAS - LeConte Glacier Deployments"> <ExperienceLayout title="CEOAS - LeConte Glacier Deployments" />
</ExperienceLayout>

View File

@@ -1,6 +1,98 @@
--- ---
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
import type { carouselGroup } from "@interfaces/image-carousel.ts";
import type { timelineEntry } from "@interfaces/timeline.ts";
import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import H2 from "@components/CustomHtmlWrappers/H2.astro";
import LinkButton from "@components/LinkButton.astro";
import PdfViewer from "@components/Media/PdfViewer.astro";
import Timeline from "@components/Timeline/Timeline.astro";
import publication from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler/ross-publication.pdf";
const headerCarouselGroup: carouselGroup = {
animation: "slide",
images: [],
};
const timeline: timelineEntry[] = [
{
event: "Started",
eventDetail: "Satellite Hardware Test Team",
date: "September 2019",
description:
"Owned test systems for four generations of Starlink flight computers and two generations of power boards",
},
{
event: "Transitioned To Remote",
eventDetail: "Moved To Oregon",
date: "August 2022",
description:
"Personal decision, but I was allowed to work on tools for the build reliability engineering team",
},
{
event: "Changed Teams",
eventDetail: "Components Test Infra Team",
date: "March 2024 - VERIFY",
description:
"Vertical move that allowed for broader application of my skills",
},
{
event: "Finished",
eventDetail: "Thanks for all the fish!",
date: "April 2025",
description:
"Celebrated five and a half years of helping put thousands of satellites, and dozens of rockets, into orbit",
},
];
--- ---
<ExperienceLayout title="CEOAS - Robotic Oceanographic Surface Sampler"> <ExperienceLayout title="CEOAS - Robotic Oceanographic Surface Sampler">
</ExperienceLayout> <Carousel carouselGroup={headerCarouselGroup} />
<div class="mt-4 flex items-center justify-center">
<LinkButton
href="https://tos.org/oceanography/article/autonomous-ctd-profiling-from-the-robotic-oceanographic-surface-sampler"
title="Official Scientific Publication"
/>
</div>
<h2 class="my-4 font-bold md:text-2xl">Summary</h2>
<h3 class="my-4 font-bold md:text-lg">Timeline</h3>
<Timeline timeline={timeline} />
<h3 class="my-4 font-bold md:text-lg">Key Takeaways</h3>
<ul class="list-inside list-disc">
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul>
<h3 class="my-4 font-bold md:text-lg">Skills Used</h3>
<div
class="border-caperren-green relative grid grid-flow-row gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
<div>
<div class="text-sm font-extrabold">Software</div>
<hr class="text-caperren-green" />
<ul class="list-inside list-disc text-sm">
<li>One</li>
<li>Two</li>
</ul>
</div>
<div>
<div class="text-sm font-extrabold">Electrical</div>
<hr class="text-caperren-green" />
</div>
<div>
<div class="text-sm font-extrabold">Mechanical</div>
<hr class="text-caperren-green" />
</div>
<div>
<div class="text-sm font-extrabold">Other</div>
<hr class="text-caperren-green" />
</div>
</div>
<H2 text="Official Scientific Publication" />
<div class="h-334">
<PdfViewer pdf={publication} />
</div>
</ExperienceLayout>

View File

@@ -2,5 +2,4 @@
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
--- ---
<ExperienceLayout title="OSURC - Officer"> <ExperienceLayout title="OSURC - Officer" />
</ExperienceLayout>

View File

@@ -2,5 +2,4 @@
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
--- ---
<ExperienceLayout title="OSURC - Electrical Team Lead"> <ExperienceLayout title="OSURC - Electrical Team Lead" />
</ExperienceLayout>

View File

@@ -2,5 +2,4 @@
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
--- ---
<ExperienceLayout title="OSURC - Emergency Software Team Lead"> <ExperienceLayout title="OSURC - Emergency Software Team Lead" />
</ExperienceLayout>

View File

@@ -1,245 +1,279 @@
--- ---
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
import Carousel from "@components/CustomCarousel/CustomCarousel.astro"; import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import type {carouselGroup} from "@interfaces/image-carousel.ts"; import type { carouselGroup } from "@interfaces/image-carousel.ts";
import YtVideo from "@components/YtVideo.astro"; import YtVideo from "@components/Media/YtVideo.astro";
import type {videoConfig} from "@interfaces/video.ts"; import type { videoConfig } from "@interfaces/video.ts";
const headerCarouselGroup: carouselGroup = { const headerCarouselGroup: carouselGroup = {
animation: "slide", animation: "slide",
images: [] images: [],
} };
const videoList: videoConfig[] = [ const videoList: videoConfig[] = [
{ {
videoTitle: "Ground Station Software Quick Overview", videoTitle: "Ground Station Software Quick Overview",
videoPath: "https://www.youtube-nocookie.com/embed/ZjGW-HWapVA" videoPath: "https://www.youtube-nocookie.com/embed/ZjGW-HWapVA",
}, },
{ {
videoTitle: "Rover Software Environment And Full Code Overview", videoTitle: "Rover Software Environment And Full Code Overview",
videoPath: "https://www.youtube-nocookie.com/embed/sceA2ZbEV8Y" videoPath: "https://www.youtube-nocookie.com/embed/sceA2ZbEV8Y",
} },
] ];
--- ---
<ExperienceLayout title="OSURC - Software Team Lead"> <ExperienceLayout title="OSURC - Software Team Lead">
<Carousel carouselGroup={headerCarouselGroup}/> <Carousel carouselGroup={headerCarouselGroup} />
<h2 class="font-bold md:text-2xl my-4 underline">Ground Station Readouts & Features</h2> <h2 class="my-4 font-bold underline md:text-2xl">
<ul class="space-y-1 list-disc list-inside"> Ground Station Readouts & Features
<li>Clock</li> </h2>
<li>Event Timer</li> <ul class="list-inside list-disc space-y-1">
<li>Status Indication <li>Clock</li>
<ul class="ps-5 space-y-1 list-disc list-inside"> <li>Event Timer</li>
<li>Rover Connection</li> <li>
<li>Controller Connection Info</li> Status Indication
<li>Radio Stats</li> <ul class="list-inside list-disc space-y-1 ps-5">
<li>GPS Stats</li> <li>Rover Connection</li>
<li>NVidia Jetson TX2 Computer Stats</li> <li>Controller Connection Info</li>
<li>Battery Voltage w/Low Battery Warning</li> <li>Radio Stats</li>
<li>Wheel Connections</li> <li>GPS Stats</li>
<li>Camera Connections</li> <li>NVidia Jetson TX2 Computer Stats</li>
</ul> <li>Battery Voltage w/Low Battery Warning</li>
<li>Wheel Connections</li>
<li>Camera Connections</li>
</ul>
</li>
<li>
Radio Direction Finding
<ul class="list-inside list-disc space-y-1 ps-5">
<li>Raw Radio RSSI Indication</li>
<li>Radio RSSI Pulse Frequency w/Validity Indication</li>
</ul>
</li>
<li>
Arm
<ul class="list-inside list-disc space-y-1 ps-5">
<li>
Special Movements
<ul class="list-inside list-disc space-y-1 ps-5">
<li>Stow Arm</li>
<li>Unstow Arm</li>
<li>Upright Arm</li>
</ul>
</li> </li>
<li>Radio Direction Finding <li>Calibrate Arm</li>
<ul class="ps-5 space-y-1 list-disc list-inside"> <li>Clear Arm Fault</li>
<li>Raw Radio RSSI Indication</li> <li>Reset Arm Motor Drivers</li>
<li>Radio RSSI Pulse Frequency w/Validity Indication</li> <li>
</ul> Task Movements
<ul class="list-inside list-disc space-y-1 ps-5">
<li>Approach Oxygen Tank</li>
<li>Depart Oxygen Tank</li>
<li>Approach Light Beacon</li>
<li>Depart Light Beacon</li>
</ul>
</li> </li>
<li>Arm </ul>
<ul class="ps-5 space-y-1 list-disc list-inside"> </li>
<li>Special Movements <li>
<ul class="ps-5 space-y-1 list-disc list-inside"> Mining/Science
<li>Stow Arm</li> <ul class="list-inside list-disc space-y-1 ps-5">
<li>Unstow Arm</li> <li>Bucket Weight Measurement</li>
<li>Upright Arm</li> <li>Bucket Lift/Tilt Position Readouts</li>
</ul> <li>
</li> Preset Bucket Movements
<li>Calibrate Arm</li> <ul class="list-inside list-disc space-y-1 ps-5">
<li>Clear Arm Fault</li> <li>Mining Transport</li>
<li>Reset Arm Motor Drivers</li> <li>Mining Measure</li>
<li>Task Movements <li>Mining Scoop</li>
<ul class="ps-5 space-y-1 list-disc list-inside"> <li>Science Panorama</li>
<li>Approach Oxygen Tank</li> <li>Mining Sample</li>
<li>Depart Oxygen Tank</li> <li>Mining Probe</li>
<li>Approach Light Beacon</li> </ul>
<li>Depart Light Beacon</li>
</ul>
</li>
</ul>
</li> </li>
<li>Mining/Science <li>
<ul class="ps-5 space-y-1 list-disc list-inside"> Science Probe Readings
<li>Bucket Weight Measurement</li> <ul class="list-inside list-disc space-y-1 ps-5">
<li>Bucket Lift/Tilt Position Readouts</li> <li>Temp in C</li>
<li>Preset Bucket Movements <li>Moisture %</li>
<ul class="ps-5 space-y-1 list-disc list-inside"> <li>Loss Tangent</li>
<li>Mining Transport</li> <li>Soil Electrical Conductivity</li>
<li>Mining Measure</li> <li>Real Dielectric Permittivity</li>
<li>Mining Scoop</li> <li>Imaginary Dielectric Permittivity</li>
<li>Science Panorama</li> </ul>
<li>Mining Sample</li>
<li>Mining Probe</li>
</ul>
</li>
<li>Science Probe Readings
<ul class="ps-5 space-y-1 list-disc list-inside">
<li>Temp in C</li>
<li>Moisture %</li>
<li>Loss Tangent</li>
<li>Soil Electrical Conductivity</li>
<li>Real Dielectric Permitivity</li>
<li>Imaginary Dielectric Permitivity</li>
</ul>
</li>
<li>Science Camera Controls
<ul class="ps-5 space-y-1 list-disc list-inside">
<li>Video Output Selection
<ul class="ps-5 space-y-1 list-disc list-inside">
<li>Network Video</li>
<li>Camera LCD</li>
</ul>
</li>
<li>Photo Controls
<ul class="ps-5 space-y-1 list-disc list-inside">
<li>Zoom In One Step</li>
<li>Zoom Out One Step</li>
<li>Full Zoom In</li>
<li>Full Zoom Out</li>
<li>Shoot Photo</li>
</ul>
</li>
</ul>
</li>
</ul>
</li> </li>
<li>SSH Console <li>
<ul class="ps-5 space-y-1 list-disc list-inside"> Science Camera Controls
<li>SSH Terminal Display</li> <ul class="list-inside list-disc space-y-1 ps-5">
<li>SSH Command Entry</li> <li>
<li>Preset Commands Video Output Selection
<ul class="ps-5 space-y-1 list-disc list-inside"> <ul class="list-inside list-disc space-y-1 ps-5">
<li>Network Host Scan</li> <li>Network Video</li>
<li>List Wifi Networks</li> <li>Camera LCD</li>
<li>Equipment Login and Help</li> </ul>
<li>Equipment Logout</li> </li>
<li>Equipment Status</li> <li>
<li>Equipment Start</li> Photo Controls
<li>Equipment Stop</li> <ul class="list-inside list-disc space-y-1 ps-5">
</ul> <li>Zoom In One Step</li>
</li> <li>Zoom Out One Step</li>
<li>Connect/Disconnect Rover Wifi by SSID</li> <li>Full Zoom In</li>
</ul> <li>Full Zoom Out</li>
<li>Shoot Photo</li>
</ul>
</li>
</ul>
</li> </li>
<li>Settings </ul>
<ul class="ps-5 space-y-1 list-disc list-inside"> </li>
<li>Map Selection</li> <li>
<li>Map Zoom Level</li> SSH Console
<li>Rover Wifi Radio Channel Selection</li> <ul class="list-inside list-disc space-y-1 ps-5">
</ul> <li>SSH Terminal Display</li>
<li>SSH Command Entry</li>
<li>
Preset Commands
<ul class="list-inside list-disc space-y-1 ps-5">
<li>Network Host Scan</li>
<li>List Wifi Networks</li>
<li>Equipment Login and Help</li>
<li>Equipment Logout</li>
<li>Equipment Status</li>
<li>Equipment Start</li>
<li>Equipment Stop</li>
</ul>
</li> </li>
<li>Mapping Display <li>Connect/Disconnect Rover Wifi by SSID</li>
<ul class="ps-5 space-y-1 list-disc list-inside"> </ul>
<li>Shows Google Map Terrain</li> </li>
<li>Shows Rover Location And Orientation</li> <li>
<li>Shows Rover GPS Coordinates</li> Settings
<li>Shows Saved Waypoints</li> <ul class="list-inside list-disc space-y-1 ps-5">
</ul> <li>Map Selection</li>
</li> <li>Map Zoom Level</li>
<li>Waypoint Entry / Editing <li>Rover Wifi Radio Channel Selection</li>
<ul class="ps-5 space-y-1 list-disc list-inside"> </ul>
<li>Name Entry For Landmarks</li> </li>
<li>GPS Entry in Decimal</li> <li>
<li>GPS Entry in Degree/Minute/Second</li> Mapping Display
<li>Waypoint Color Choice</li> <ul class="list-inside list-disc space-y-1 ps-5">
</ul> <li>Shows Google Map Terrain</li>
</li> <li>Shows Rover Location And Orientation</li>
<li>Navigation Waypoints <li>Shows Rover GPS Coordinates</li>
<ul class="ps-5 space-y-1 list-disc list-inside"> <li>Shows Saved Waypoints</li>
<li>Shows And Allows Editing Of Nav Waypoints</li> </ul>
</ul> </li>
</li> <li>
<li>Landmark Waypoints Waypoint Entry / Editing
<ul class="ps-5 space-y-1 list-disc list-inside"> <ul class="list-inside list-disc space-y-1 ps-5">
<li>Shows And Allows Editing Of Landmark Waypoints</li> <li>Name Entry For Landmarks</li>
</ul> <li>GPS Entry in Decimal</li>
</li> <li>GPS Entry in Degree/Minute/Second</li>
<li>Arm Joint Positions <li>Waypoint Color Choice</li>
<ul class="ps-5 space-y-1 list-disc list-inside"> </ul>
<li>Positions Of Six Arm Joints In Revolutions</li> </li>
</ul> <li>
</li> Navigation Waypoints
<li>Gripper Joint Positions <ul class="list-inside list-disc space-y-1 ps-5">
<ul class="ps-5 space-y-1 list-disc list-inside"> <li>Shows And Allows Editing Of Nav Waypoints</li>
<li>Positions Shown As Raw Encoder Positions</li> </ul>
</ul> </li>
</li> <li>
<li>Arm Motor Drive Statuses Landmark Waypoints
<ul class="ps-5 space-y-1 list-disc list-inside"> <ul class="list-inside list-disc space-y-1 ps-5">
<li>Communication/Movement/Fault Statuses For All Six Arm Joints</li> <li>Shows And Allows Editing Of Landmark Waypoints</li>
</ul> </ul>
</li> </li>
<li>Gripper Mode Readouts <li>
<ul class="ps-5 space-y-1 list-disc list-inside"> Arm Joint Positions
<li>Gripper Mode Control State</li> <ul class="list-inside list-disc space-y-1 ps-5">
</ul> <li>Positions Of Six Arm Joints In Revolutions</li>
</li> </ul>
<li>Xbox Control Mode </li>
<ul class="ps-5 space-y-1 list-disc list-inside"> <li>
<li>Showed Whether Xbox Controller Moving Arm Or Mining</li> Gripper Joint Positions
</ul> <ul class="list-inside list-disc space-y-1 ps-5">
</li> <li>Positions Shown As Raw Encoder Positions</li>
<li>Heading and Goal Indication w/Compass </ul>
<ul class="ps-5 space-y-1 list-disc list-inside"> </li>
<li>Raw Heading Indication</li> <li>
<li>Goal Indication (Unused)</li> Arm Motor Drive Statuses
<li>Compass Heading Indication</li> <ul class="list-inside list-disc space-y-1 ps-5">
</ul> <li>Communication/Movement/Fault Statuses For All Six Arm Joints</li>
</li> </ul>
<li>Low Resolution Mode </li>
<ul class="ps-5 space-y-1 list-disc list-inside"> <li>
<li>Controlled Low Resolution Fallback Mode During Radio Failure</li> Gripper Mode Readouts
</ul> <ul class="list-inside list-disc space-y-1 ps-5">
</li> <li>Gripper Mode Control State</li>
<li>Current Speed </ul>
<ul class="ps-5 space-y-1 list-disc list-inside"> </li>
<li>GPS Speed</li> <li>
</ul> Xbox Control Mode
</li> <ul class="list-inside list-disc space-y-1 ps-5">
<li>Speed Limit <li>Showed Whether Xbox Controller Moving Arm Or Mining</li>
<ul class="ps-5 space-y-1 list-disc list-inside"> </ul>
<li>% Of Max Rover Speed As Limit</li> </li>
</ul> <li>
</li> Heading and Goal Indication w/Compass
<li>Tank Drive Output <ul class="list-inside list-disc space-y-1 ps-5">
<ul class="ps-5 space-y-1 list-disc list-inside"> <li>Raw Heading Indication</li>
<li>% Of Total Power To Left/Right Rover Drive Systems</li> <li>Goal Indication (Unused)</li>
</ul> <li>Compass Heading Indication</li>
</li> </ul>
<li>IMU Readings </li>
<ul class="ps-5 space-y-1 list-disc list-inside"> <li>
<li>Pitch/Roll Readings In +/- 1 Readout</li> Low Resolution Mode
</ul> <ul class="list-inside list-disc space-y-1 ps-5">
</li> <li>Controlled Low Resolution Fallback Mode During Radio Failure</li>
<li>Triple Camera Displays </ul>
<ul class="ps-5 space-y-1 list-disc list-inside"> </li>
<li>One Primary Video Display</li> <li>
<li>Two Secondary Video Displays</li> Current Speed
<li>Named Display For Currently Viewed Camera</li> <ul class="list-inside list-disc space-y-1 ps-5">
<li>Ability To Set Each Display To Any Camera</li> <li>GPS Speed</li>
<li>Ability to Disable Any Camera</li> </ul>
<li>Ability to Pan/Tilt Any Camera</li> </li>
</ul> <li>
</li> Speed Limit
</ul> <ul class="list-inside list-disc space-y-1 ps-5">
<h2 class="font-bold md:text-2xl my-4 underline">Rover Demos and Software Overviews</h2> <li>% Of Max Rover Speed As Limit</li>
{videoList.map((video) => ( </ul>
<h3 class="font-bold md:text-lg my-4">{video.videoTitle}</h3> </li>
<YtVideo videoConfig={video}/> <li>
))} Tank Drive Output
<ul class="list-inside list-disc space-y-1 ps-5">
</ExperienceLayout> <li>% Of Total Power To Left/Right Rover Drive Systems</li>
</ul>
</li>
<li>
IMU Readings
<ul class="list-inside list-disc space-y-1 ps-5">
<li>Pitch/Roll Readings In +/- 1 Readout</li>
</ul>
</li>
<li>
Triple Camera Displays
<ul class="list-inside list-disc space-y-1 ps-5">
<li>One Primary Video Display</li>
<li>Two Secondary Video Displays</li>
<li>Named Display For Currently Viewed Camera</li>
<li>Ability To Set Each Display To Any Camera</li>
<li>Ability to Disable Any Camera</li>
<li>Ability to Pan/Tilt Any Camera</li>
</ul>
</li>
</ul>
<h2 class="my-4 font-bold underline md:text-2xl">
Rover Demos and Software Overviews
</h2>
{
videoList.map((video) => (
<div>
<h3 class="my-4 font-bold md:text-lg">{video.videoTitle}</h3>
<YtVideo videoConfig={video} />
</div>
))
}
</ExperienceLayout>

View File

@@ -2,5 +2,4 @@
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
--- ---
<ExperienceLayout title="SARL - Dechorionator"> <ExperienceLayout title="SARL - Dechorionator" />
</ExperienceLayout>

View File

@@ -2,5 +2,4 @@
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
--- ---
<ExperienceLayout title="SARL - Denso Embryo Pick and Plate"> <ExperienceLayout title="SARL - Denso Embryo Pick and Plate" />
</ExperienceLayout>

View File

@@ -2,5 +2,4 @@
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
--- ---
<ExperienceLayout title="SARL - Shuttlebox Behavior System"> <ExperienceLayout title="SARL - Shuttlebox Behavior System" />
</ExperienceLayout>

View File

@@ -2,5 +2,4 @@
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
--- ---
<ExperienceLayout title="SARL - Team Lead"> <ExperienceLayout title="SARL - Team Lead" />
</ExperienceLayout>

View File

@@ -2,5 +2,4 @@
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
--- ---
<ExperienceLayout title="SARL - Zebrafish Embryo Pick and Plate"> <ExperienceLayout title="SARL - Zebrafish Embryo Pick and Plate" />
</ExperienceLayout>

View File

@@ -2,5 +2,4 @@
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
--- ---
<ExperienceLayout title="SARL - ZScan Processor"> <ExperienceLayout title="SARL - ZScan Processor" />
</ExperienceLayout>

View File

@@ -1,78 +1,80 @@
--- ---
import ExperienceLayout from '@layouts/ExperienceLayout.astro'; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
import Timeline from '@components/Timeline/Timeline.astro'; import Timeline from "@components/Timeline/Timeline.astro";
import Carousel from "@components/CustomCarousel/CustomCarousel.astro"; import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import spring_2019_interns import spring_2019_interns from "@assets/experience/spacex/avionics-test-engineering-internship/spring-2019-interns.jpg";
from "@assets/experience/spacex/avionics-test-engineering-internship/spring-2019-interns.jpg";
import type {carouselGroup} from "@interfaces/image-carousel.ts"; import type { carouselGroup } from "@interfaces/image-carousel.ts";
import type {timelineEntry} from "@interfaces/timeline.ts"; import type { timelineEntry } from "@interfaces/timeline.ts";
const headerCarouselGroup: carouselGroup = { const headerCarouselGroup: carouselGroup = {
animation: "slide", animation: "slide",
images: [ images: [spring_2019_interns],
spring_2019_interns };
]
}
const timeline: timelineEntry[] = [ const timeline: timelineEntry[] = [
{ {
event: "Started", event: "Started",
date: "January 2019", date: "January 2019",
}, },
{ {
event: "Finished", event: "Finished",
date: "March 2019", date: "March 2019",
} },
]; ];
--- ---
<ExperienceLayout title="SpaceX - Avionics Test Engineering Internship"> <ExperienceLayout title="SpaceX - Avionics Test Engineering Internship">
<Carousel carouselGroup={headerCarouselGroup}/> <Carousel carouselGroup={headerCarouselGroup} />
<h2 class="font-bold md:text-2xl my-4">Summary</h2> <h2 class="my-4 font-bold md:text-2xl">Summary</h2>
<h3 class="font-bold md:text-lg my-4">Timeline</h3> <h3 class="my-4 font-bold md:text-lg">Timeline</h3>
<Timeline timeline={timeline}/> <Timeline timeline={timeline} />
<h3 class="font-bold md:text-lg my-4">Key Takeaways</h3> <h3 class="my-4 font-bold md:text-lg">Key Takeaways</h3>
<ul class="list-disc list-inside"> <ul class="list-inside list-disc">
<li></li> <li></li>
</ul> </ul>
<h3 class="font-bold md:text-lg my-4">Skills Used</h3> <h3 class="my-4 font-bold md:text-lg">Skills Used</h3>
<div class="relative grid gap-6 grid-flow-row sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 border-caperren-green"> <div
<div> class="border-caperren-green relative grid grid-flow-row gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
<div class="font-extrabold text-sm">Software</div> >
<hr class="text-caperren-green"/> <div>
<ul class="list-disc list-inside text-sm"> <div class="text-sm font-extrabold">Software</div>
<li>Python</li> <hr class="text-caperren-green" />
<li>Test Driven Development</li> <ul class="list-inside list-disc text-sm">
</ul> <li>Python</li>
</div> <li>Test Driven Development</li>
<div> </ul>
<div class="font-extrabold text-sm">Electrical</div>
<hr class="text-caperren-green"/>
</div>
<div>
<div class="font-extrabold text-sm">Other</div>
<hr class="text-caperren-green"/>
</div>
</div> </div>
<h2 class="font-bold md:text-2xl my-4">Details</h2> <div>
<div class="text-sm font-extrabold">Electrical</div>
<hr class="text-caperren-green" />
</div>
<div>
<div class="text-sm font-extrabold">Other</div>
<hr class="text-caperren-green" />
</div>
</div>
<h2 class="my-4 font-bold md:text-2xl">Details</h2>
Though I did get to work on some really fun projects during my internship at
Though I did get to work on some really fun projects during my internship at SpaceX, I unfortunately cant go into SpaceX, I unfortunately cant go into much detail due to NDAs and ITAR
much detail due to NDAs and ITAR restrictions. What I can say is that I mainly wrote Python for a new avionics restrictions. What I can say is that I mainly wrote Python for a new avionics
hardware test system. My experience with writing Python in the numerous other projects Ive done really helped me hardware test system. My experience with writing Python in the numerous other
out here, as the framework SpaceX has created was quite complex and would otherwise have been fairly difficult to projects Ive done really helped me out here, as the framework SpaceX has
write code for. I also wrote a simple tool for automating the creation of Jira work tickets so that the two teams created was quite complex and would otherwise have been fairly difficult to
that ended up using it wouldnt have to have their members manually creating dozens of them as work and issues came write code for. I also wrote a simple tool for automating the creation of Jira
in through a separate system. work tickets so that the two teams that ended up using it wouldnt have to
have their members manually creating dozens of them as work and issues came in
I was also quite happy in that I got to perform some circuit debugging on avionics test system hardware, both for my through a separate system. I was also quite happy in that I got to perform
project and for a separate test system. A final experience I had here was getting to work directly with the head some circuit debugging on avionics test system hardware, both for my project
engineer from a company that supplied a piece of test hardware I was interfacing with. It was quite incredible to and for a separate test system. A final experience I had here was getting to
see just how much weight a SpaceX email address had when trying to solve problems I had found with the hardware. Not work directly with the head engineer from a company that supplied a piece of
only were they responsive, but in fact were willing to fast-track firmware updates for us to get things working. test hardware I was interfacing with. It was quite incredible to see just how
Coming from clubs and small labs where a support email might not even get a response for months, it was quite a much weight a SpaceX email address had when trying to solve problems I had
refreshing experience. found with the hardware. Not only were they responsive, but in fact were
willing to fast-track firmware updates for us to get things working. Coming
</ExperienceLayout> from clubs and small labs where a support email might not even get a response
for months, it was quite a refreshing experience.
</ExperienceLayout>

View File

@@ -1,90 +1,98 @@
--- ---
import ExperienceLayout from '@layouts/ExperienceLayout.astro'; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
import Timeline from '@components/Timeline/Timeline.astro'; import Timeline from "@components/Timeline/Timeline.astro";
import Carousel from "@components/CustomCarousel/CustomCarousel.astro"; import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import starlink_headquarters_selfie import starlink_headquarters_selfie from "@assets/experience/spacex/hardware-test-engineer-i-ii/starlink-headquarters-selfie.jpg";
from "@assets/experience/spacex/hardware-test-engineer-i-ii/starlink-headquarters-selfie.jpg";
import type {carouselGroup} from "@interfaces/image-carousel.ts"; import type { carouselGroup } from "@interfaces/image-carousel.ts";
import type {timelineEntry} from "@interfaces/timeline.ts"; import type { timelineEntry } from "@interfaces/timeline.ts";
const headerCarouselGroup: carouselGroup = { const headerCarouselGroup: carouselGroup = {
animation: "slide", animation: "slide",
images: [ images: [starlink_headquarters_selfie],
starlink_headquarters_selfie };
]
}
const timeline: timelineEntry[] = [ const timeline: timelineEntry[] = [
{ {
event: "Started", event: "Started",
eventDetail: "Satellite Hardware Test Team", eventDetail: "Satellite Hardware Test Team",
date: "September 2019", date: "September 2019",
description: "Owned test systems for four generations of Starlink flight computers and two generations of power boards" description:
}, "Owned test systems for four generations of Starlink flight computers and two generations of power boards",
{ },
event: "Transitioned To Remote", {
eventDetail: "Moved To Oregon", event: "Transitioned To Remote",
date: "August 2022", eventDetail: "Moved To Oregon",
description: "Personal decision, but I was allowed to work on tools for the build reliability engineering team" date: "August 2022",
}, description:
{ "Personal decision, but I was allowed to work on tools for the build reliability engineering team",
event: "Changed Teams", },
eventDetail: "Components Test Infra Team", {
date: "March 2024 - VERIFY", event: "Changed Teams",
description: "Vertical move that allowed for broader application of my skills" eventDetail: "Components Test Infra Team",
}, date: "March 2024 - VERIFY",
{ description:
event: "Finished", "Vertical move that allowed for broader application of my skills",
eventDetail: "Thanks for all the fish!", },
date: "April 2025", {
description: "Celebrated five and a half years of helping put thousands of satellites, and dozens of rockets, into orbit" event: "Finished",
} eventDetail: "Thanks for all the fish!",
date: "April 2025",
description:
"Celebrated five and a half years of helping put thousands of satellites, and dozens of rockets, into orbit",
},
]; ];
--- ---
<ExperienceLayout title="SpaceX - Hardware Test Engineer I/II"> <ExperienceLayout title="SpaceX - Hardware Test Engineer I/II">
<Carousel carouselGroup={headerCarouselGroup}/> <Carousel carouselGroup={headerCarouselGroup} />
<h2 class="font-bold md:text-2xl my-4">Summary</h2> <h2 class="my-4 font-bold md:text-2xl">Summary</h2>
<h3 class="font-bold md:text-lg my-4">Timeline</h3> <h3 class="my-4 font-bold md:text-lg">Timeline</h3>
<Timeline timeline={timeline}/> <Timeline timeline={timeline} />
<h3 class="font-bold md:text-lg my-4">Key Takeaways</h3> <h3 class="my-4 font-bold md:text-lg">Key Takeaways</h3>
<ul class="list-disc list-inside"> <ul class="list-inside list-disc">
<li>Created test systems which validated ~4500 Starlink satellite flight computers, and ~4000 power boards</li> <li>
<li>Developed program-critical infrastructure that enabled efficient triage, management, and tracking of Created test systems which validated ~4500 Starlink satellite flight
hardware failures computers, and ~4000 power boards
</li> </li>
<li>Designed and deployed automated, unified, and containerized infrastructure to greatly increase application <li>
reliability and development speed Developed program-critical infrastructure that enabled efficient triage,
</li> management, and tracking of hardware failures
</ul> </li>
<h3 class="font-bold md:text-lg my-4">Skills Used</h3> <li>
<div class="relative grid gap-6 grid-flow-row sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 border-caperren-green"> Designed and deployed automated, unified, and containerized infrastructure
<div> to greatly increase application reliability and development speed
<div class="font-extrabold text-sm">Software</div> </li>
<hr class="text-caperren-green"/> </ul>
<ul class="list-disc list-inside text-sm"> <h3 class="my-4 font-bold md:text-lg">Skills Used</h3>
<li>Python</li> <div
<li>Test Driven Development</li> class="border-caperren-green relative grid grid-flow-row gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
</ul> >
</div> <div>
<div> <div class="text-sm font-extrabold">Software</div>
<div class="font-extrabold text-sm">Electrical</div> <hr class="text-caperren-green" />
<hr class="text-caperren-green"/> <ul class="list-inside list-disc text-sm">
</div> <li>Python</li>
<div> <li>Test Driven Development</li>
<div class="font-extrabold text-sm">Mechanical</div> </ul>
<hr class="text-caperren-green"/>
</div>
<div>
<div class="font-extrabold text-sm">Other</div>
<hr class="text-caperren-green"/>
</div>
</div> </div>
<div>
<div class="text-sm font-extrabold">Electrical</div>
<hr class="text-caperren-green" />
</div>
<div>
<div class="text-sm font-extrabold">Mechanical</div>
<hr class="text-caperren-green" />
</div>
<div>
<div class="text-sm font-extrabold">Other</div>
<hr class="text-caperren-green" />
</div>
</div>
<h2 class="font-bold md:text-2xl my-4">Details By Team</h2> <h2 class="my-4 font-bold md:text-2xl">Details By Team</h2>
<h3 class="font-bold md:text-lg my-4">Starlink Hardware Test</h3> <h3 class="my-4 font-bold md:text-lg">Starlink Hardware Test</h3>
<h3 class="font-bold md:text-lg my-4">Build Reliability Engineering</h3> <h3 class="my-4 font-bold md:text-lg">Build Reliability Engineering</h3>
<h3 class="font-bold md:text-lg my-4">Components Test Infrastructure</h3> <h3 class="my-4 font-bold md:text-lg">Components Test Infrastructure</h3>
</ExperienceLayout>
</ExperienceLayout>

View File

@@ -1,56 +1,62 @@
--- ---
import HobbyLayout from "@layouts/HobbyLayout.astro"; import HobbyLayout from "@layouts/HobbyLayout.astro";
import Carousel from "@components/CustomCarousel/CustomCarousel.astro"; import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import type {carouselGroup} from "@interfaces/image-carousel.ts"; import type { carouselGroup } from "@interfaces/image-carousel.ts";
import injection_site from "@assets/hobby/body-mods/rfid-implant/injection-site.jpg"; import injection_site from "@assets/hobby/body-mods/rfid-implant/injection-site.jpg";
import injector_exploded from "@assets/hobby/body-mods/rfid-implant/injector-exploded.jpg"; import injector_exploded from "@assets/hobby/body-mods/rfid-implant/injector-exploded.jpg";
import quarter_euro_transponder from "@assets/hobby/body-mods/rfid-implant/quarter-euro-transponder.png"; import quarter_euro_transponder from "@assets/hobby/body-mods/rfid-implant/quarter-euro-transponder.png";
import xem_pouch from "@assets/hobby/body-mods/rfid-implant/xem-pouch.jpg"; import xem_pouch from "@assets/hobby/body-mods/rfid-implant/xem-pouch.jpg";
const rfidImplantCarouselGroup: carouselGroup = { const rfidImplantCarouselGroup: carouselGroup = {
animation: "slide", animation: "slide",
images: [ images: [
xem_pouch, xem_pouch,
injector_exploded, injector_exploded,
quarter_euro_transponder, quarter_euro_transponder,
injection_site injection_site,
] ],
} };
--- ---
<HobbyLayout title="Body Mods"> <HobbyLayout title="Body Mods">
<h2 class="font-bold md:text-2xl my-4 underline">RFID Implant</h2> <h2 class="my-4 font-bold underline md:text-2xl">RFID Implant</h2>
<Carousel carouselGroup={rfidImplantCarouselGroup}/> <Carousel carouselGroup={rfidImplantCarouselGroup} />
<p class="mt-4"> <p class="mt-4">
Back when I was in college, a few of my friends and I got this crazy idea to all get RFID implants together. Back when I was in college, a few of my friends and I got this crazy idea to
They are essentially the same things you'd use to microchip a pet, but with a slightly different firmware all get RFID implants together. They are essentially the same things you'd
configuration, allowing scans with any 125KHz-compatible reader. The implants came from <a use to microchip a pet, but with a slightly different firmware
class="text-blue-500 hover:text-blue-300" href="https://dangerousthings.com/product/xem/">dangerousthings.com</a>, configuration, allowing scans with any 125KHz-compatible reader. The
and we were lucky enough to have a vet-med student as a friend who made the installation a quick and painless implants came from <a
process! class="text-blue-500 hover:text-blue-300"
I'm glad that I'm not afraid of needles, as the 16 gauge injector the kit came with was nothing to scoff at. href="https://dangerousthings.com/product/xem/">dangerousthings.com</a
Since healing, you would never know the implant was there, with the site leaving no scar or visible indication >, and we were lucky enough to have a vet-med student as a friend who made
of its presence. the installation a quick and painless process! I'm glad that I'm not afraid
</p> of needles, as the 16 gauge injector the kit came with was nothing to scoff
<p class="mt-4"> at. Since healing, you would never know the implant was there, with the site
With that out of the way, our group began work on hardware which would support the new implants. leaving no scar or visible indication of its presence.
The goal was to have a generic usb-keyboard emulator for typing passwords with a valid scan, a car </p>
off-acc-on ignition replacement, and a fairly specialized modification to the OSU Robotics Club's doorway <p class="mt-4">
scanning system so they would support these on top of the official OSU ID cards. With that out of the way, our group began work on hardware which would
As tends to happen, life got busy, and only the usb-keyboard emulator actually came to fruition. The electronics support the new implants. The goal was to have a generic usb-keyboard
and emulator for typing passwords with a valid scan, a car off-acc-on ignition
primary firmware were handled by <a replacement, and a fairly specialized modification to the OSU Robotics
class="text-blue-500 hover:text-blue-300" Club's doorway scanning system so they would support these on top of the
href="https://nickmccomb.net">Nick McComb</a>, official OSU ID cards. As tends to happen, life got busy, and only the
enclosure by usb-keyboard emulator actually came to fruition. The electronics and primary
<a class="text-blue-500 hover:text-blue-300" href="https://dylanthrush.com">Dylan Thrush</a>, and I supported firmware were handled by <a
some minor firmware development and debugging. If you want to see an example of the keyboard emulator unlocking class="text-blue-500 hover:text-blue-300"
a PC, check out the video on <a href="https://nickmccomb.net">Nick McComb</a
class="text-blue-500 hover:text-blue-300" >, enclosure by
href="https://nickmccomb.net/college/printed-circuit-boards/computer-access-module">Nick's website</a>! <a class="text-blue-500 hover:text-blue-300" href="https://dylanthrush.com"
</p> >Dylan Thrush</a
</HobbyLayout> >, and I supported some minor firmware development and debugging. If you
want to see an example of the keyboard emulator unlocking a PC, check out
the video on <a
class="text-blue-500 hover:text-blue-300"
href="https://nickmccomb.net/college/printed-circuit-boards/computer-access-module"
>Nick's website</a
>!
</p>
</HobbyLayout>

View File

@@ -2,5 +2,4 @@
import HobbyLayout from "@layouts/HobbyLayout.astro"; import HobbyLayout from "@layouts/HobbyLayout.astro";
--- ---
<HobbyLayout title="Homelab - Home Automation"> <HobbyLayout title="Homelab - Home Automation" />
</HobbyLayout>

View File

@@ -1,29 +1,22 @@
--- ---
import HobbyLayout from "@layouts/HobbyLayout.astro"; import HobbyLayout from "@layouts/HobbyLayout.astro";
import Carousel from "@components/CustomCarousel/CustomCarousel.astro"; import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import type {carouselGroup} from "@interfaces/image-carousel.ts"; import type { carouselGroup } from "@interfaces/image-carousel.ts";
import rack_from_above from "@assets/hobby/homelab/home-server-rack/rack-from-above.jpg"; import rack_from_above from "@assets/hobby/homelab/home-server-rack/rack-from-above.jpg";
import rack_from_below from "@assets/hobby/homelab/home-server-rack/rack-from-below.jpg"; import rack_from_below from "@assets/hobby/homelab/home-server-rack/rack-from-below.jpg";
import rack_middle from "@assets/hobby/homelab/home-server-rack/rack-middle.jpg"; import rack_middle from "@assets/hobby/homelab/home-server-rack/rack-middle.jpg";
import rack_top from "@assets/hobby/homelab/home-server-rack/rack-top.jpg"; import rack_top from "@assets/hobby/homelab/home-server-rack/rack-top.jpg";
const headerCarouselGroup: carouselGroup = { const headerCarouselGroup: carouselGroup = {
animation: "slide", animation: "slide",
images: [ images: [rack_from_below, rack_from_above, rack_top, rack_middle],
rack_from_below, };
rack_from_above,
rack_top,
rack_middle
]
}
--- ---
<HobbyLayout title="Homelab - Home Server Rack"> <HobbyLayout title="Homelab - Home Server Rack">
<Carousel carouselGroup={headerCarouselGroup}/> <Carousel carouselGroup={headerCarouselGroup} />
<!--<h2 class="font-bold md:text-2xl my-4">Prior Homelab</h2>--> <!--<h2 class="font-bold md:text-2xl my-4">Prior Homelab</h2>-->
</HobbyLayout> </HobbyLayout>

View File

@@ -1,23 +1,19 @@
--- ---
import HobbyLayout from "@layouts/HobbyLayout.astro"; import HobbyLayout from "@layouts/HobbyLayout.astro";
import Carousel from "@components/CustomCarousel/CustomCarousel.astro"; import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import type {carouselGroup} from "@interfaces/image-carousel.ts"; import type { carouselGroup } from "@interfaces/image-carousel.ts";
import cluster_and_switch from "@assets/hobby/homelab/kubernetes-cluster/cluster-and-switch.jpg"; import cluster_and_switch from "@assets/hobby/homelab/kubernetes-cluster/cluster-and-switch.jpg";
const headerCarouselGroup: carouselGroup = { const headerCarouselGroup: carouselGroup = {
animation: "slide", animation: "slide",
images: [ images: [cluster_and_switch],
cluster_and_switch };
]
}
--- ---
<HobbyLayout title="Homelab - Kubernetes Cluster"> <HobbyLayout title="Homelab - Kubernetes Cluster">
<Carousel carouselGroup={headerCarouselGroup}/> <Carousel carouselGroup={headerCarouselGroup} />
<!--<h2 class="font-bold md:text-2xl my-4">Prior Homelab</h2>--> <!--<h2 class="font-bold md:text-2xl my-4">Prior Homelab</h2>-->
</HobbyLayout> </HobbyLayout>

View File

@@ -1,8 +1,8 @@
--- ---
import HobbyLayout from "@layouts/HobbyLayout.astro"; import HobbyLayout from "@layouts/HobbyLayout.astro";
import Carousel from "@components/CustomCarousel/CustomCarousel.astro"; import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import type {carouselGroup} from "@interfaces/image-carousel.ts"; import type { carouselGroup } from "@interfaces/image-carousel.ts";
import enclosure_front from "@assets/hobby/homelab/offsite-backup-rack/enclosure-front.jpg"; import enclosure_front from "@assets/hobby/homelab/offsite-backup-rack/enclosure-front.jpg";
import enclosure_front_pc_panel_open from "@assets/hobby/homelab/offsite-backup-rack/enclosure-front-pc-panel-open.jpg"; import enclosure_front_pc_panel_open from "@assets/hobby/homelab/offsite-backup-rack/enclosure-front-pc-panel-open.jpg";
@@ -12,35 +12,32 @@ import enclosure_right from "@assets/hobby/homelab/offsite-backup-rack/enclosure
import enclosure_with_ups from "@assets/hobby/homelab/offsite-backup-rack/enclosure-with-ups.jpg"; import enclosure_with_ups from "@assets/hobby/homelab/offsite-backup-rack/enclosure-with-ups.jpg";
import power_adapter_tray_and_dc_dc from "@assets/hobby/homelab/offsite-backup-rack/power-adapter-tray-and-dc-dc.jpg"; import power_adapter_tray_and_dc_dc from "@assets/hobby/homelab/offsite-backup-rack/power-adapter-tray-and-dc-dc.jpg";
import power_supply_closeup from "@assets/hobby/homelab/offsite-backup-rack/power-supply-closeup.jpg"; import power_supply_closeup from "@assets/hobby/homelab/offsite-backup-rack/power-supply-closeup.jpg";
import power_supply_mounting_location import power_supply_mounting_location from "@assets/hobby/homelab/offsite-backup-rack/power-supply-mounting-location.jpg";
from "@assets/hobby/homelab/offsite-backup-rack/power-supply-mounting-location.jpg";
import sata_tight_fit from "@assets/hobby/homelab/offsite-backup-rack/sata-tight-fit.jpg"; import sata_tight_fit from "@assets/hobby/homelab/offsite-backup-rack/sata-tight-fit.jpg";
import sff_pc_with_sata_and_usb_ssds from "@assets/hobby/homelab/offsite-backup-rack/sff-pc-with-sata-and-usb-ssds.jpg"; import sff_pc_with_sata_and_usb_ssds from "@assets/hobby/homelab/offsite-backup-rack/sff-pc-with-sata-and-usb-ssds.jpg";
import up_and_running from "@assets/hobby/homelab/offsite-backup-rack/up-and-running.png"; import up_and_running from "@assets/hobby/homelab/offsite-backup-rack/up-and-running.png";
const headerCarouselGroup: carouselGroup = { const headerCarouselGroup: carouselGroup = {
animation: "slide", animation: "slide",
images: [ images: [
enclosure_front, enclosure_front,
enclosure_front_pc_panel_open, enclosure_front_pc_panel_open,
enclosure_left, enclosure_left,
enclosure_rear, enclosure_rear,
enclosure_right, enclosure_right,
sff_pc_with_sata_and_usb_ssds, sff_pc_with_sata_and_usb_ssds,
sata_tight_fit, sata_tight_fit,
power_adapter_tray_and_dc_dc, power_adapter_tray_and_dc_dc,
enclosure_with_ups, enclosure_with_ups,
power_supply_mounting_location, power_supply_mounting_location,
power_supply_closeup, power_supply_closeup,
up_and_running up_and_running,
] ],
} };
--- ---
<HobbyLayout title="Homelab - Offsite Backup Rack"> <HobbyLayout title="Homelab - Offsite Backup Rack">
<Carousel carouselGroup={headerCarouselGroup}/> <Carousel carouselGroup={headerCarouselGroup} />
<!--<h2 class="font-bold md:text-2xl my-4">Prior Homelab</h2>--> <!--<h2 class="font-bold md:text-2xl my-4">Prior Homelab</h2>-->
</HobbyLayout> </HobbyLayout>

View File

@@ -1,73 +1,74 @@
--- ---
import HobbyLayout from "@layouts/HobbyLayout.astro"; import HobbyLayout from "@layouts/HobbyLayout.astro";
import Carousel from "@components/CustomCarousel/CustomCarousel.astro"; import LinkButton from "@components/LinkButton.astro";
import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import type {carouselGroup} from "@interfaces/image-carousel.ts"; import type { carouselGroup } from "@interfaces/image-carousel.ts";
import bottom_fasteners_installed import bottom_fasteners_installed from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/bottom-fasteners-installed.jpg";
from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/bottom-fasteners-installed.jpg";
import closed_seam from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/closed-seam.jpg"; import closed_seam from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/closed-seam.jpg";
import closed_top_buttons_installed import closed_top_buttons_installed from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/closed-top-buttons-installed.jpg";
from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/closed-top-buttons-installed.jpg"; import inside_top_and_bottom from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/inside-top-and-bottom.jpg";
import inside_top_and_bottom import inside_top_and_bottom_buttons_installed from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/inside-top-and-bottom-buttons-installed.jpg";
from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/inside-top-and-bottom.jpg"; import inside_top_and_bottom_with_buttons from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/inside-top-and-bottom-with-buttons.jpg";
import inside_top_and_bottom_buttons_installed import installed_on_bike_handlebars_reference from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/installed-on-bike-handlebars-reference.jpg";
from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/inside-top-and-bottom-buttons-installed.jpg"; import installed_on_bike_riders_position from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/installed-on-bike-riders-position.jpg";
import inside_top_and_bottom_with_buttons
from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/inside-top-and-bottom-with-buttons.jpg";
import installed_on_bike_handlebars_reference
from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/installed-on-bike-handlebars-reference.jpg";
import installed_on_bike_riders_position
from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/installed-on-bike-riders-position.jpg";
import top_and_bottom from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/top-and-bottom.jpg"; import top_and_bottom from "@assets/hobby/motorcycling/custom-accessories/chubby-buttons-2-mount/top-and-bottom.jpg";
const headerCarouselGroup: carouselGroup = { const headerCarouselGroup: carouselGroup = {
animation: "slide", animation: "slide",
images: [ images: [
installed_on_bike_riders_position, installed_on_bike_riders_position,
installed_on_bike_handlebars_reference, installed_on_bike_handlebars_reference,
closed_top_buttons_installed, closed_top_buttons_installed,
bottom_fasteners_installed, bottom_fasteners_installed,
closed_seam, closed_seam,
inside_top_and_bottom_buttons_installed, inside_top_and_bottom_buttons_installed,
inside_top_and_bottom_with_buttons, inside_top_and_bottom_with_buttons,
inside_top_and_bottom, inside_top_and_bottom,
top_and_bottom top_and_bottom,
] ],
} };
--- ---
<HobbyLayout title="Motorcycling - Chubby Buttons 2 Mount"> <HobbyLayout title="Motorcycling - Chubby Buttons 2 Mount">
<Carousel carouselGroup={headerCarouselGroup}/> <Carousel carouselGroup={headerCarouselGroup} />
<div class="flex items-center justify-center mt-4"> <div class="mt-4 flex items-center justify-center">
<a class="bg-black rounded-2xl p-2 border-2 text-caperren-green border-caperren-green hover:border-caperren-green-light hover:text-caperren-green-light" <LinkButton
href="https://cad.onshape.com/documents/816b0b1bef7883d4dc25c66c/v/e11fe68753e080b72015cfb8/e/3802abbd9d7b7c4d2c7ebad3"> href="https://cad.onshape.com/documents/816b0b1bef7883d4dc25c66c/v/e11fe68753e080b72015cfb8/e/3802abbd9d7b7c4d2c7ebad3"
Onshape title="Onshape CAD Design Files"
Design Files</a> />
</div> </div>
<p class="mt-4"> <p class="mt-4">
Having ridden motorcycles since I was sixteen, and being an avid music enjoyer, I'd been looking for a way to Having ridden motorcycles since I was sixteen, and being an avid music
improve my music listening experience while on-the-go. One large pain-point I'd always had was with controlling enjoyer, I'd been looking for a way to improve my music listening experience
track selection and volume levels while my gloves were on, as smartphones don't respond very well to this, if at while on-the-go. One large pain-point I'd always had was with controlling
all. In 2023 I found out about chubby buttons, a low-power and highly water-resistant media controller track selection and volume levels while my gloves were on, as smartphones
specifically designed for use with gloves! The only problem was that it was designed to be worn on your arm don't respond very well to this, if at all. In 2023 I found out about chubby
using a strap, which isn't very practical on a motorcycle. buttons, a low-power and highly water-resistant media controller
</p> specifically designed for use with gloves! The only problem was that it was
<p class="mt-4"> designed to be worn on your arm using a strap, which isn't very practical on
Having recently gotten a 3D Printer, and having some baseline modelling skills, I purchased one, took some a motorcycle.
measurements, and began designing a proper mount. </p>
I already owned and used many 1" RAM compatible mounts and gear on the bike, so I decided to make this one <p class="mt-4">
natively When starting this project, I'd recently gotten a 3D Printer, so having some
support the ball size to use an existing clamp I had stored away. This design was the first where I decided to baseline modelling skills I took some measurements, and began designing a
use heat-set inserts in the plastic, along with some medium-strength loctite on the fasteners, due to the proper mount. I already owned and used many 1" RAM compatible mounts and
high-vibration environment the mount would see. The print was also done using a UV resistant, high-temp rated, gear on my bikes, so I decided to make this one natively support the ball
and non-water-absorbing ASA filament, as the direct expose to the elements would not allow something like cheap size to use an existing clamp I had stored away. This design was the first
PLA to last very long. where I decided to use heat-set inserts in the plastic, along with some
</p> medium-strength Loctite on the fasteners, due to the high-vibration
<p class="mt-4"> environment the mount would see. The print was also done using a UV
While my first iteration was sized appropriately and went together with no issues, the ball mount neck ended up resistant, high-temp rated, and non-water-absorbing ASA filament, as the
snapping due to a low infill percentage. After changing that area to 100% infill, including a handful of the direct expose to the elements would not allow something like cheap PLA to
rear mount layers that it attached to, a second iteration has worked perfectly for a few years now! If you're last very long.
interested in printing this yourself, feel free to download the model using the button under the photos! </p>
</p> <p class="mt-4">
</HobbyLayout> While my first iteration was sized appropriately and went together with no
issues, the ball mount neck ended up snapping due to a low infill
percentage. After changing that area to 100% infill, including a handful of
the rear mount layers that it attached to, a second iteration has worked
perfectly for a few years now! If you're interested in printing this
yourself, feel free to download the model using the button under the photos!
</p>
</HobbyLayout>

View File

@@ -1,8 +1,8 @@
--- ---
import HobbyLayout from "@layouts/HobbyLayout.astro"; import HobbyLayout from "@layouts/HobbyLayout.astro";
import Carousel from "@components/CustomCarousel/CustomCarousel.astro"; import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import type {carouselGroup} from "@interfaces/image-carousel.ts"; import type { carouselGroup } from "@interfaces/image-carousel.ts";
import kz750 from "@assets/hobby/motorcycling/lineup/1979-kawasaki-kz750-senior-photo.jpg"; import kz750 from "@assets/hobby/motorcycling/lineup/1979-kawasaki-kz750-senior-photo.jpg";
import ninja600 from "@assets/hobby/motorcycling/lineup/1991-kawasaki-ninja-600r.jpg"; import ninja600 from "@assets/hobby/motorcycling/lineup/1991-kawasaki-ninja-600r.jpg";
@@ -11,33 +11,49 @@ import drz400 from "@assets/hobby/motorcycling/lineup/2005-suzuki-drz-400.jpg";
import fjr1300 from "@assets/hobby/motorcycling/lineup/2015-fjr-1300-mountaintop.jpg"; import fjr1300 from "@assets/hobby/motorcycling/lineup/2015-fjr-1300-mountaintop.jpg";
import sg400 from "@assets/hobby/motorcycling/lineup/2021-csc-sg400.jpg"; import sg400 from "@assets/hobby/motorcycling/lineup/2021-csc-sg400.jpg";
const fjrCarouselGroup: carouselGroup = {
const fjrCarouselGroup: carouselGroup = {animation: "slide", images: [fjr1300]} animation: "slide",
const cscCarouselGroup: carouselGroup = {animation: "slide", images: [sg400]} images: [fjr1300],
const drzCarouselGroup: carouselGroup = {animation: "slide", images: [drz400]} };
const concoursCarouselGroup: carouselGroup = {animation: "slide", images: [concours]} const cscCarouselGroup: carouselGroup = { animation: "slide", images: [sg400] };
const ninjaCarouselGroup: carouselGroup = {animation: "slide", images: [ninja600]} const drzCarouselGroup: carouselGroup = {
const kz750CarouselGroup: carouselGroup = {animation: "slide", images: [kz750]} animation: "slide",
images: [drz400],
};
const concoursCarouselGroup: carouselGroup = {
animation: "slide",
images: [concours],
};
const ninjaCarouselGroup: carouselGroup = {
animation: "slide",
images: [ninja600],
};
const kz750CarouselGroup: carouselGroup = {
animation: "slide",
images: [kz750],
};
--- ---
<HobbyLayout title="Motorcycling - Lineup"> <HobbyLayout title="Motorcycling - Lineup">
<h2 class="font-bold md:text-2xl my-4 underline">Current Lineup</h2> <h2 class="my-4 font-bold underline md:text-2xl">Current Lineup</h2>
<h3 class="font-bold md:text-lg my-4 underline">2015 Yamaha FJR 1300</h3> <h3 class="my-4 font-bold underline md:text-lg">2015 Yamaha FJR 1300</h3>
<Carousel carouselGroup={fjrCarouselGroup}/> <Carousel carouselGroup={fjrCarouselGroup} />
<h3 class="font-bold md:text-lg my-4 underline">2021 CSC SG400</h3> <h3 class="my-4 font-bold underline md:text-lg">2021 CSC SG400</h3>
<Carousel carouselGroup={cscCarouselGroup}/> <Carousel carouselGroup={cscCarouselGroup} />
<h2 class="font-bold md:text-2xl my-4 underline">Prior Lineup</h2> <h2 class="my-4 font-bold underline md:text-2xl">Prior Lineup</h2>
<h3 class="font-bold md:text-lg my-4 underline">2005 Suzuki DRZ 400</h3> <h3 class="my-4 font-bold underline md:text-lg">2005 Suzuki DRZ 400</h3>
<Carousel carouselGroup={drzCarouselGroup}/> <Carousel carouselGroup={drzCarouselGroup} />
<h3 class="font-bold md:text-lg my-4 underline">1991 Kawasaki Concours ZG1000</h3> <h3 class="my-4 font-bold underline md:text-lg">
<Carousel carouselGroup={concoursCarouselGroup}/> 1991 Kawasaki Concours ZG1000
</h3>
<Carousel carouselGroup={concoursCarouselGroup} />
<h3 class="font-bold md:text-lg my-4 underline">1979 Kawasaki KZ750</h3> <h3 class="my-4 font-bold underline md:text-lg">1979 Kawasaki KZ750</h3>
<Carousel carouselGroup={kz750CarouselGroup}/> <Carousel carouselGroup={kz750CarouselGroup} />
<h3 class="font-bold md:text-lg my-4 underline">1991 Kawasaki Ninja 600R</h3> <h3 class="my-4 font-bold underline md:text-lg">1991 Kawasaki Ninja 600R</h3>
<Carousel carouselGroup={ninjaCarouselGroup}/> <Carousel carouselGroup={ninjaCarouselGroup} />
</HobbyLayout> </HobbyLayout>

View File

@@ -2,5 +2,4 @@
import HobbyLayout from "@layouts/HobbyLayout.astro"; import HobbyLayout from "@layouts/HobbyLayout.astro";
--- ---
<HobbyLayout> <HobbyLayout />
</HobbyLayout>

View File

@@ -2,5 +2,4 @@
import HobbyLayout from "@layouts/HobbyLayout.astro"; import HobbyLayout from "@layouts/HobbyLayout.astro";
--- ---
<HobbyLayout> <HobbyLayout />
</HobbyLayout>

View File

@@ -2,5 +2,4 @@
import HobbyLayout from "@layouts/HobbyLayout.astro"; import HobbyLayout from "@layouts/HobbyLayout.astro";
--- ---
<HobbyLayout title="NixOS"> <HobbyLayout title="NixOS" />
</HobbyLayout>

View File

@@ -1,79 +1,96 @@
--- ---
import BaseLayout from '../layouts/BaseLayout.astro'; import BaseLayout from "../layouts/BaseLayout.astro";
import Carousel from "@components/CustomCarousel/CustomCarousel.astro"; import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import type {carouselGroup} from "@interfaces/image-carousel.ts"; import type { carouselGroup } from "@interfaces/image-carousel.ts";
import alaska_bike_mountain_ocean from "@assets/about/alaska-bike-mountain-ocean.jpg" import alaska_bike_mountain_ocean from "@assets/about/alaska-bike-mountain-ocean.jpg";
import circ_champions from "@assets/about/circ-champions.jpg" import circ_champions from "@assets/about/circ-champions.jpg";
import headshot from "@assets/about/headshot.jpg"; import headshot from "@assets/about/headshot.jpg";
const headerCarouselGroup: carouselGroup = { const headerCarouselGroup: carouselGroup = {
animation: "slide", animation: "slide",
images: [ images: [headshot, alaska_bike_mountain_ocean, circ_champions],
headshot, };
alaska_bike_mountain_ocean,
circ_champions
]
}
--- ---
<BaseLayout title="About" showTitle={false}> <BaseLayout title="About" showTitle={false}>
<Carousel carouselGroup={headerCarouselGroup}/> <Carousel carouselGroup={headerCarouselGroup} />
<h2 class="font-bold md:text-2xl my-4 underline">Who Am I</h2> <h2 class="my-4 font-bold underline md:text-2xl">Who Am I</h2>
<p> <p>
My name is Corwin Perren, and I'm a multi-disciplinary engineer with a <a My name is Corwin Perren, and I'm a multi-disciplinary engineer with a <a
class="text-blue-500 hover:text-blue-300" href="/education">degree in computer science</a> from Oregon State class="text-blue-500 hover:text-blue-300"
University. href="/education">degree in computer science</a
For as long as I can remember, I've been fascinated by how things work, never being shy about taking them apart > from Oregon State University. For as long as I can remember, I've been fascinated
to learn the gritty details. At a young age, I began tinkering, adding lights and fans and doorbells to the by how things work, never being shy about taking them apart to learn the gritty
pretend cardboard box houses my brother and I would play in. details. At a young age, I began tinkering, adding lights and fans and doorbells
Later, I learned to solder, work on vehicles and engines, install and run Linux, manage enterprise computing to the pretend cardboard box houses my brother and I would play in. Later, I learned
infrastructure, build and repair computers, write scripts, and by the end of high school set out with a clear to solder, work on vehicles and engines, install and run Linux, manage enterprise
goal for my college years. computing infrastructure, build and repair computers, write scripts, and by the
I wanted to learn and teach myself enough to be able to think up almost any project, encompassing all facets of end of high school set out with a clear goal for my college years. I wanted to
engineering, and be capable of driving it to completion with my own skill set. learn and teach myself enough to be able to think up almost any project, encompassing
</p> all facets of engineering, and be capable of driving it to completion with my
<p class="mt-4"> own skill set.
I think young me would be very pleased by how well I managed to achieve that goal! </p>
Through college, I learned electronics and PCB design, embedded and pc programming, basic mechanical design and <p class="mt-4">
fabrication, on top of learning how to work well with others in a team. I think young me would be very pleased by how well I managed to achieve that
I quickly realized that robotics was an ideal focus due to its inherent multi-disciplinary nature, and joined goal! Through college, I learned electronics and PCB design, embedded and pc
the OSU Robotics Club, which introduced me to people who are still my best friends today. programming, basic mechanical design and fabrication, on top of learning how
Through student engineering jobs, I had the unique opportunity to work on some incredible projects such as the to work well with others in a team. I quickly realized that robotics was an
<a class="text-blue-500 hover:text-blue-300" ideal focus due to its inherent multi-disciplinary nature, and joined the
href="/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler">robotic oceanographic OSU Robotics Club, which introduced me to people who are still my best
surface sampler</a> and an <a class="text-blue-500 hover:text-blue-300" friends today. Through student engineering jobs, I had the unique
href="/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate">embryo opportunity to work on some incredible projects such as the
pick-and-plate machine</a>. <a
One my my proudest moments was when our club's mars rover took first place at the Candian International Rover class="text-blue-500 hover:text-blue-300"
Challenge in 2018, for which I was the <a class="text-blue-500 hover:text-blue-300" href="/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler"
href="/experience/osu-robotics-club/mars-rover-software-team-lead">software >robotic oceanographic surface sampler</a
lead</a>! > and an <a
</p> class="text-blue-500 hover:text-blue-300"
<p class="mt-4"> href="/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate"
After a short three-month <a class="text-blue-500 hover:text-blue-300" >embryo pick-and-plate machine</a
href="/experience/spacex/avionics-test-engineering-internship">internship</a> at >. One my my proudest moments was when our club's mars rover took first
SpaceX in Hawthorne at the end of college, I applied for a <a class="text-blue-500 hover:text-blue-300" place at the Candian International Rover Challenge in 2018, for which I was
href="/experience/spacex/hardware-test-engineer-i-ii">test the <a
engineering</a> position with the company's Starlink team and was hired in mid-2019. class="text-blue-500 hover:text-blue-300"
For six years, I developed test system hardware, software, harnesses, mechanical fixtures, devops href="/experience/osu-robotics-club/mars-rover-software-team-lead"
infrastructure, websites, and tooling to ensure that Starlink, Falcon, Dragon, and Starship component tests were >software lead</a
producing well-validated and reliable hardware. >!
Through it all, I got to apply and hone every skill I had developed, while learning countless more. </p>
Now though, it's on to the next adventure, whatever that may be! <p class="mt-4">
</p> After a short three-month <a
<p class="mt-4"> class="text-blue-500 hover:text-blue-300"
To learn more about my experiences, hobbies, interests, and skills, feel free to explore the site! href="/experience/spacex/avionics-test-engineering-internship"
While the short summary above provides some insight into who I am, it leaves out plenty! >internship</a
For example, I've been an avid <a class="text-blue-500 hover:text-blue-300" href="/hobby/motorcycling/lineup">motorcycle > at SpaceX in Hawthorne at the end of college, I applied for a <a
rider</a> since I was sixteen, and have an <a class="text-blue-500 hover:text-blue-300" href="/hobby/body-mods">rfid class="text-blue-500 hover:text-blue-300"
implant</a> in my hand! href="/experience/spacex/hardware-test-engineer-i-ii">test engineering</a
</p> > position with the company's Starlink team and was hired in mid-2019. For six
<p class="mt-4"> years, I developed test system hardware, software, harnesses, mechanical fixtures,
If you're interested in contacting me, feel free to message on <a class="text-blue-500 hover:text-blue-300" devops infrastructure, websites, and tooling to ensure that Starlink, Falcon,
href="https://github.com/caperren">LinkedIn</a>, Dragon, and Starship component tests were producing well-validated and reliable
or via the primary contact methods hardware. Through it all, I got to apply and hone every skill I had developed,
on my <a class="text-blue-500 hover:text-blue-300" href="/resume/2025-11-10-infrastructure-engineer">resume</a>. while learning countless more. Now though, it's on to the next adventure, whatever
</p> that may be!
</BaseLayout> </p>
<p class="mt-4">
To learn more about my experiences, hobbies, interests, and skills, feel
free to explore the site! While the short summary above provides some
insight into who I am, it leaves out plenty! For example, I've been an avid <a
class="text-blue-500 hover:text-blue-300"
href="/hobby/motorcycling/lineup">motorcycle rider</a
> since I was sixteen, and have an <a
class="text-blue-500 hover:text-blue-300"
href="/hobby/body-mods">rfid implant</a
> in my hand!
</p>
<p class="mt-4">
If you're interested in contacting me, feel free to message on <a
class="text-blue-500 hover:text-blue-300"
href="https://github.com/caperren">LinkedIn</a
>, or via the primary contact methods on my <a
class="text-blue-500 hover:text-blue-300"
href="/resume/2025-11-10-infrastructure-engineer">resume</a
>.
</p>
</BaseLayout>

View File

@@ -1,6 +1,6 @@
--- ---
import ResumeLayout from "@layouts/ResumeLayout.astro"; import ResumeLayout from "@layouts/ResumeLayout.astro";
import resume from "@assets/resume/corwin_perren_2019-07-01_hardware_test_engineer.pdf" import resume from "@assets/resume/corwin_perren_2019-07-01_hardware_test_engineer.pdf";
--- ---
<ResumeLayout title="2019-07-01 - Hardware Test Engineer" resume={resume}/> <ResumeLayout title="2019-07-01 - Hardware Test Engineer" resume={resume} />

View File

@@ -2,4 +2,4 @@
import ResumeLayout from "@layouts/ResumeLayout.astro"; import ResumeLayout from "@layouts/ResumeLayout.astro";
--- ---
<ResumeLayout/> <ResumeLayout />

View File

@@ -1,6 +1,6 @@
--- ---
import ResumeLayout from "@layouts/ResumeLayout.astro"; import ResumeLayout from "@layouts/ResumeLayout.astro";
import resume from "@assets/resume/corwin_perren_2025-10-27-infrastructure_engineer.pdf" import resume from "@assets/resume/corwin_perren_2025-10-27-infrastructure_engineer.pdf";
--- ---
<ResumeLayout title="2025-10-27 - Infrastructure Engineer" resume={resume}/> <ResumeLayout title="2025-10-27 - Infrastructure Engineer" resume={resume} />

View File

@@ -1,4 +1,4 @@
import type {APIRoute} from 'astro'; import type { APIRoute } from "astro";
const getRobotsTxt = (sitemapURL: URL) => `\ const getRobotsTxt = (sitemapURL: URL) => `\
User-agent: * User-agent: *
@@ -7,7 +7,7 @@ Allow: /
Sitemap: ${sitemapURL.href} Sitemap: ${sitemapURL.href}
`; `;
export const GET: APIRoute = ({site}) => { export const GET: APIRoute = ({ site }) => {
const sitemapURL = new URL('sitemap-index.xml', site); const sitemapURL = new URL("sitemap-index.xml", site);
return new Response(getRobotsTxt(sitemapURL)); return new Response(getRobotsTxt(sitemapURL));
}; };

View File

@@ -1 +1 @@
import "flowbite"; import "flowbite";

View File

@@ -5,9 +5,9 @@
@source "../../node_modules/flowbite"; @source "../../node_modules/flowbite";
@theme { @theme {
--default-font-family: font-mono; --default-font-family: font-mono;
--color-caperren-green: #10ac25; --color-caperren-green: #10ac25;
--color-caperren-green-light: #00ff2a; --color-caperren-green-light: #00ff2a;
--color-caperren-green-dark: #06370e; --color-caperren-green-dark: #06370e;
} }

View File

@@ -1,16 +1,15 @@
import {test, expect} from '@playwright/test'; import { test, expect } from "@playwright/test";
import {getPaths} from "@data/site-layout.ts";
import { getPaths } from "@data/site-layout.ts";
for (const pagePath of getPaths()) { for (const pagePath of getPaths()) {
test(`${pagePath}: Navigable`, async ({page}) => { test(`${pagePath}: Navigable`, async ({ page }) => {
const response = await page.request.get(pagePath); const response = await page.request.get(pagePath);
await expect(response).toBeOK(); await expect(response).toBeOK();
}); });
test(`${pagePath}: Has Title`, async ({page}) => { test(`${pagePath}: Has Title`, async ({ page }) => {
await page.goto(pagePath); await page.goto(pagePath);
expect(await page.title()).not.toBe("Corwin Perren") expect(await page.title()).not.toBe("Corwin Perren");
}); });
} }

View File

@@ -1,39 +1,45 @@
import {expect, test} from "vitest"; import { expect, test } from "vitest";
import {siteLayout, getPaths} from "@data/site-layout.ts"; import { siteLayout, getPaths } from "@data/site-layout.ts";
export const setDifference = <T>(a: Set<T>, b: Set<T>) => export const setDifference = <T>(a: Set<T>, b: Set<T>) =>
new Set([...a].filter(x => !b.has(x))); new Set([...a].filter((x) => !b.has(x)));
// Paths that should be known to Astro statically // Paths that should be known to Astro statically
const astroStaticPaths = new Set( const astroStaticPaths = new Set(
Object.keys(import.meta.glob("/src/pages/**/*.astro")) Object.keys(import.meta.glob("/src/pages/**/*.astro")).map(
.map((path) => (path) =>
path path
.replace("/src/pages", "") .replace("/src/pages", "")
.replace(/index\.astro$/, "") .replace(/index\.astro$/, "")
.replace(/\.astro$|\.md$/, "") .replace(/\.astro$|\.md$/, "")
.replace(/\/$/, "") .replace(/\/$/, "") || "/",
|| "/" ),
)); );
// Paths that exist in the site layout // Paths that exist in the site layout
const siteLayoutPaths = new Set([...getPaths(siteLayout), ...getPaths(siteLayout, [], true)]); const siteLayoutPaths = new Set([
...getPaths(siteLayout),
...getPaths(siteLayout, [], true),
]);
test('Astro Paths Not Empty', () => { test("Astro Paths Not Empty", () => {
expect(astroStaticPaths).not.toHaveLength(0); expect(astroStaticPaths).not.toHaveLength(0);
}); });
test('Site Layout Paths Not Empty', () => { test("Site Layout Paths Not Empty", () => {
expect(siteLayoutPaths).not.toHaveLength(0); expect(siteLayoutPaths).not.toHaveLength(0);
}); });
test('Pages Missing from Site Layout', () => { test("Pages Missing from Site Layout", () => {
const astroNotLayoutPaths = setDifference(astroStaticPaths, siteLayoutPaths); const astroNotLayoutPaths = setDifference(astroStaticPaths, siteLayoutPaths);
expect(astroNotLayoutPaths).toHaveLength(0); expect(astroNotLayoutPaths).toHaveLength(0);
}); });
test('Pages Missing from Astro Paths', () => { test("Pages Missing from Astro Paths", () => {
const siteLayoutNotAstroPaths = setDifference(siteLayoutPaths, astroStaticPaths); const siteLayoutNotAstroPaths = setDifference(
expect(siteLayoutNotAstroPaths).toHaveLength(0); siteLayoutPaths,
astroStaticPaths,
);
expect(siteLayoutNotAstroPaths).toHaveLength(0);
}); });

View File

@@ -1,32 +1,15 @@
{ {
"compilerOptions": { "compilerOptions": {
"paths": { "paths": {
"@assets/*": [ "@assets/*": ["./src/assets/*"],
"./src/assets/*" "@components/*": ["./src/components/*"],
], "@data/*": ["./src/data/*"],
"@components/*": [ "@interfaces/*": ["./src/interfaces/*"],
"./src/components/*" "@layouts/*": ["./src/layouts/*"],
], "@styles/*": ["./src/styles/*"]
"@data/*": [
"./src/data/*"
],
"@interfaces/*": [
"./src/interfaces/*"
],
"@layouts/*": [
"./src/layouts/*"
],
"@styles/*": [
"./src/styles/*"
]
} }
}, },
"extends": "astro/tsconfigs/strict", "extends": "astro/tsconfigs/strict",
"include": [ "include": [".astro/types.d.ts", "**/*"],
".astro/types.d.ts", "exclude": ["dist"]
"**/*"
],
"exclude": [
"dist"
]
} }

View File

@@ -1,22 +1,19 @@
import path from 'path'; import path from "path";
import {getViteConfig} from 'astro/config'; import { getViteConfig } from "astro/config";
export default getViteConfig( export default getViteConfig(
{ {
// @ts-ignore // @ts-ignore
test: { test: {
exclude: [ exclude: ["test-e2e/**", "node_modules/**"],
"test-e2e/**", resolve: {
"node_modules/**", alias: {
], "@": path.resolve(__dirname, "./src"),
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
},
},
}, },
},
}, },
{ },
site: 'https://caperren.com/' {
}, site: "https://caperren.com/",
); },
);