Merge pull request 'Component for PCBs, many visual tweaks, finished dechorionator content, added many many photos, started work on mars rover software lead, timeline to luxon and automatic date-based ordering' (#18) from website-content-updates into main
All checks were successful
Build and Test - Production / test (push) Successful in 5m12s
Build and Test - Production / build_and_push (push) Successful in 4m24s
Build and Test - Production / deploy_production (push) Successful in 2s

Reviewed-on: #18
This commit was merged in pull request #18.
This commit is contained in:
2025-12-13 07:23:40 +00:00
68 changed files with 594 additions and 109 deletions

View File

@@ -15,7 +15,8 @@
cleanup-check \ cleanup-check \
cleanup-code \ cleanup-code \
convert_video \ convert_video \
convert_video_times convert_video_times \
generate_asset_imports
default: dev default: dev
@@ -89,3 +90,9 @@ convert_video_times:
-qp 28 \ -qp 28 \
-an \ -an \
$(output) $(output)
generate_asset_imports:
@for assets_path in `find "src/assets/${assets_relative_path}" -maxdepth 1 -type f -printf "%f\n"`; do \
without_extension=$${assets_path/%.*}; \
echo "import $${without_extension//-/_} from \"@assets/${assets_relative_path}/$$assets_path\";"; \
done;

View File

@@ -13,13 +13,17 @@ Concours
CONSERV CONSERV
Corwin Corwin
dangerousthings dangerousthings
dechorionation
Dechorionator Dechorionator
dechorionators
dockerization dockerization
dockerizing dockerizing
drumheller drumheller
ebox ebox
ELMI
fhhs fhhs
flowbite flowbite
flowrate
Gitea Gitea
HDFS HDFS
headshot headshot
@@ -34,6 +38,8 @@ leconte
Loctite Loctite
luxon luxon
MGMT MGMT
Micropumps
Millis
Mokai Mokai
Multimeters Multimeters
nixos nixos
@@ -42,26 +48,32 @@ Onshape
OSSM OSSM
OSURC OSURC
Passthroughs Passthroughs
pcbs
Perren Perren
Perren's Perren's
Pixhawk Pixhawk
Protocase Protocase
pubpath pubpath
RFID RFID
Rito
RSSI RSSI
SARL SARL
showerheads
Shuttlebox Shuttlebox
sinnhuber sinnhuber
sitemapindex sitemapindex
Smartsheet Smartsheet
solderable
ssds ssds
Starlink Starlink
steller steller
Steller Steller
Tanguay
Teamcenter Teamcenter
timelapse timelapse
triaging triaging
trivago trivago
Truong
Unstow Unstow
uuidv uuidv
vaapi vaapi

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 MiB

View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@@ -1,15 +1,21 @@
--- ---
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import type { ComponentPropsBase } from "@interfaces/components.ts";
import type { carouselGroup } from "@interfaces/image-carousel.ts"; import type { carouselGroup } from "@interfaces/image-carousel.ts";
const groupToShow: carouselGroup = Astro.props.carouselGroup; interface Props extends ComponentPropsBase {
carouselGroup: carouselGroup;
showBorder?: boolean;
}
const { class: className, carouselGroup, showBorder = true } = Astro.props;
--- ---
<custom-carousel <custom-carousel
class="relative flex w-full flex-col" class="relative flex w-full flex-col"
data-custom-carousel={groupToShow.animation} data-custom-carousel={carouselGroup.animation ?? "slide"}
data-custom-carousel-interval={groupToShow.interval} data-custom-carousel-interval={carouselGroup.interval}
> >
<!-- Modal for fullscreen viewing --> <!-- Modal for fullscreen viewing -->
<div <div
@@ -57,54 +63,63 @@ const groupToShow: carouselGroup = Astro.props.carouselGroup;
</div> </div>
<!-- Carousel wrapper --> <!-- Carousel wrapper -->
<div class="relative h-56 w-full overflow-hidden md:h-120"> <div
class:list={[
"relative h-56 w-full overflow-hidden rounded-lg md:h-120",
showBorder ? "border" : false,
className ? className : "border-caperren-green-dark",
]}
>
{ {
groupToShow.images.map((image, index) => ( carouselGroup.images &&
<div carouselGroup.images.map((image, index) => (
class="hidden duration-1500 ease-in-out" <div
data-custom-carousel-item={index} class="hidden bg-black duration-1500 ease-in-out"
> data-custom-carousel-item={index}
<Image >
src={image} <Image
class="absolute inset-0 m-auto h-full max-h-full w-auto max-w-full object-contain" src={image}
alt="..." class="absolute inset-0 m-auto h-full max-h-full w-auto max-w-full object-contain"
layout="constrained" alt="..."
loading="eager" layout="constrained"
/> loading="eager"
</div> />
)) </div>
))
} }
</div> </div>
<!-- Slider indicators --> <!-- Slider indicators -->
{ {
groupToShow.images.length > 1 && ( carouselGroup.images && carouselGroup.images.length > 1 && (
<div> <div>
<div class="absolute bottom-2 left-1/2 z-30 flex -translate-x-1/2 space-x-3 rounded-full"> <div class="absolute bottom-2 left-1/2 z-30 flex -translate-x-1/2 space-x-3 rounded-full">
{groupToShow.images.map((_, index) => ( {carouselGroup.images &&
<button carouselGroup.images.map((_, index) => (
type="button" <button
class="hover:bg-caperren-green-light h-3 w-3 rounded-full bg-black" type="button"
aria-current={index ? "false" : "true"} class="hover:bg-caperren-green-light h-3 w-3 rounded-full"
aria-label={index.toString()} aria-current={index ? "false" : "true"}
data-custom-carousel-slide-to={index} aria-label={index.toString()}
/> data-custom-carousel-slide-to={index}
))} />
))}
</div> </div>
<button <button
type="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" 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 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"> <span class="ring-caperren-green/35 group-hover:ring-caperren-green inline-flex h-10 w-10 items-center justify-center rounded-full bg-black/35 ring-2 group-hover:bg-black/75">
<svg <svg
class="text-caperren-green group-hover:text-caperren-green-light h-4 w-4" class="text-caperren-green stroke-caperren-green group-hover:text-caperren-green-light h-4 w-4"
aria-hidden="true" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 6 10" viewBox="0 0 8 10"
> >
<path <path
stroke="currentColor" stroke="currentColor"
fill-opacity="0%"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
@@ -119,15 +134,16 @@ const groupToShow: carouselGroup = Astro.props.carouselGroup;
class="group absolute end-0 top-0 z-30 flex h-full cursor-pointer items-center justify-center px-4 focus:outline-none" 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 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"> <span class="ring-caperren-green/25 group-hover:ring-caperren-green inline-flex h-10 w-10 items-center justify-center rounded-full bg-black/25 ring-2 group-hover:bg-black/75">
<svg <svg
class="text-caperren-green group-hover:text-caperren-green-light h-4 w-4" class="text-caperren-green stroke-caperren-green group-hover:text-caperren-green-light h-4 w-4"
aria-hidden="true" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 6 10" viewBox="0 0 4 10"
> >
<path <path
stroke="currentColor" stroke="currentColor"
fill-opacity="0%"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"

View File

@@ -22,7 +22,7 @@ const {
<div class="mx-auto my-auto"> <div class="mx-auto my-auto">
<video <video
class="h-auto w-full" class="border-caperren-green-dark h-auto w-full rounded-lg border"
controls={controls} controls={controls}
autoplay={autoPlay} autoplay={autoPlay}
loop={loop} loop={loop}

View File

@@ -1,5 +1,5 @@
--- ---
import type { videoConfig } from "@interfaces/yt-video.ts"; import type { videoConfig } from "@interfaces/video.ts";
interface Props extends videoConfig {} interface Props extends videoConfig {}
@@ -18,7 +18,7 @@ const aspect = `${width} / ${height}`;
<iframe <iframe
src={videoPath} src={videoPath}
title={videoTitle} title={videoTitle}
class="h-full w-full" class="border-caperren-green-dark h-full w-full rounded-lg border"
allow={"accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; " + allow={"accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; " +
(autoPlay ? "autoplay;" : "")} (autoPlay ? "autoplay;" : "")}
referrerpolicy="strict-origin-when-cross-origin" referrerpolicy="strict-origin-when-cross-origin"

View File

@@ -36,7 +36,8 @@ const { pathname } = Astro.url;
data-dropdown-offset-skidding="12" data-dropdown-offset-skidding="12"
class:list={[ class:list={[
"hover:text-caperren-green-light lg:hover:text-caperren-green-light flex w-full items-center justify-between lg:p-0 lg:hover:bg-transparent", "hover:text-caperren-green-light lg:hover:text-caperren-green-light flex w-full items-center justify-between lg:p-0 lg:hover:bg-transparent",
pathname.startsWith(getHrefPath(paths, entry)) pathname.startsWith(getHrefPath(paths, entry)) &&
!(entry.placeholderEntry ?? false)
? "border-caperren-green border-b-2" ? "border-caperren-green border-b-2"
: false, : false,
]} ]}
@@ -71,20 +72,32 @@ const { pathname } = Astro.url;
) : ( ) : (
<div> <div>
<a <a
href={getHrefPath(paths, entry)} href={
!(entry.placeholderEntry ?? false)
? getHrefPath(paths, entry)
: undefined
}
target={ target={
getHrefPath(paths, entry).startsWith("/") ? "" : "_blank" getHrefPath(paths, entry).startsWith("/") ? "" : "_blank"
} }
class:list={[ class:list={[
"hover:text-caperren-green-light ring-caperren-green-dark block bg-transparent lg:p-0", "ring-caperren-green-dark block bg-transparent lg:p-0",
pathname === getHrefPath(paths, entry) pathname === getHrefPath(paths, entry)
? "border-caperren-green border-b-2" ? "border-caperren-green border-b-2"
: false, : false,
entry.isSubItem ? "ms-3" : false,
entry.placeholderEntry
? false
: "hover:text-caperren-green-light",
]} ]}
aria-current={ aria-current={
pathname === getHrefPath(paths, entry) ? "page" : undefined pathname === getHrefPath(paths, entry) &&
!(entry.placeholderEntry ?? false)
? "page"
: undefined
} }
> >
{entry.isSubItem && "∟ "}
{entry.navText} {entry.navText}
</a> </a>
</div> </div>

View File

@@ -3,7 +3,7 @@ const hasHeader = Astro.slots.has("header");
const hasDefault = Astro.slots.has("default"); const hasDefault = Astro.slots.has("default");
--- ---
<div class="grid grid-cols-1 gap-3"> <div class="grid grid-cols-1 gap-0.5">
{ {
Astro.slots.has("header") && ( Astro.slots.has("header") && (
<div> <div>

View File

@@ -0,0 +1,51 @@
---
import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import Ul from "@components/Ul.astro";
import type {
printedCircuitBoard,
printedCircuitBoardRevision,
} from "@interfaces/printed-circuit-board.ts";
interface Props {
pcb: printedCircuitBoard;
}
const { pcb } = Astro.props;
const semanticPcbRevisionSort = (
a: printedCircuitBoardRevision,
b: printedCircuitBoardRevision,
): number =>
-((a.major - b.major) * 100 + (a.minor - b.minor) * 10 + (a.patch - b.patch));
---
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{
pcb.revisions?.sort(semanticPcbRevisionSort).map((revision) => (
<div class="border-caperren-green block space-y-2 rounded-lg border bg-black py-2">
<div class="border-caperren-green flex flex-wrap items-center justify-between rounded-none border-b px-4 pb-2">
<div>
<span class="font-black">Revision:</span>
<span>
{revision.major}.{revision.minor}.{revision.patch}
</span>
</div>
<div class="text-sm italic">{revision.date.toISODate()}</div>
</div>
<div class="px-4">
<Carousel
class=""
carouselGroup={{ images: revision.images }}
showBorder={false}
/>
</div>
{revision.notes && revision.notes.length > 0 && (
<Ul
class="border-caperren-green border-t p-4 text-sm"
lineItems={revision.notes}
/>
)}
</div>
))
}
</div>

View File

@@ -1,7 +1,11 @@
--- ---
import type { timelineEntry } from "@interfaces/timeline.ts"; import type { timelineEntry } from "@interfaces/timeline.ts";
const timeline: timelineEntry[] = Astro.props.timeline || []; interface Props {
timeline: timelineEntry[];
}
const { timeline = [] } = Astro.props;
--- ---
<custom-timeline> <custom-timeline>
@@ -11,21 +15,25 @@ const timeline: timelineEntry[] = Astro.props.timeline || [];
data-timeline data-timeline
> >
{ {
timeline.map((entry, index) => ( timeline
<div .sort((a: timelineEntry, b: timelineEntry) =>
class="border-caperren-green min-w-s max-w-s rounded-lg border bg-black px-2 pt-1 pb-2" a.date.diff(b.date).toMillis(),
data-timeline-node-index={index} )
> .map((entry, index) => (
<h3 class="text-lg font-bold">{entry.event}</h3> <div
<h4 class="leading-none font-semibold">{entry.eventDetail}</h4> class="border-caperren-green min-w-s max-w-s rounded-lg border bg-black px-2 pt-1 pb-2"
<time class="mt-1 mb-2 text-sm leading-none italic"> data-timeline-node-index={index}
{entry.date} >
</time> <h3 class="text-lg font-bold">{entry.event}</h3>
{entry.description && ( <h4 class="leading-none font-semibold">{entry.eventDetail}</h4>
<p class="text-sm font-normal">{entry.description}</p> <time class="mt-1 mb-2 text-sm leading-none italic">
)} {entry.date.toFormat("LLLL kkkk")}
</div> </time>
)) {entry.description && (
<p class="text-sm font-normal">{entry.description}</p>
)}
</div>
))
} }
</div> </div>
<div <div

0
src/content.config.ts Normal file
View File

View File

@@ -1,7 +1,6 @@
import type { navLink } from "@interfaces/site-layout.ts"; import type { navLink } from "@interfaces/site-layout.ts";
export const siteLayout: navLink[] = [ export const siteLayout: navLink[] = [
// Standard navbar entries
{ navText: "About", path: "" }, { navText: "About", path: "" },
{ navText: "Education", path: "education" }, { navText: "Education", path: "education" },
{ {
@@ -26,49 +25,57 @@ export const siteLayout: navLink[] = [
navText: "OSU CEOAS Ocean Mixing Group", navText: "OSU CEOAS Ocean Mixing Group",
path: "osu-ceoas-ocean-mixing-group", path: "osu-ceoas-ocean-mixing-group",
children: [ children: [
{
navText: "Student Software/Electrical Engineer",
placeholderEntry: true,
},
{ {
navText: "Robotic Oceanographic Surface Sampler", navText: "Robotic Oceanographic Surface Sampler",
isSubItem: true,
path: "robotic-oceanographic-surface-sampler", path: "robotic-oceanographic-surface-sampler",
}, },
{ {
navText: "LeConte Glacier Deployments", navText: "LeConte Glacier Deployments",
isSubItem: true,
path: "leconte-glacier-deployments", path: "leconte-glacier-deployments",
}, },
], ],
}, },
{ {
enabled: false, navText: "OSU Sinnhuber Aquatic Research Lab",
navText: "OSU SARL",
path: "osu-sinnhuber-aquatic-research-laboratory", path: "osu-sinnhuber-aquatic-research-laboratory",
children: [ children: [
{ {
enabled: false, navText: "Student Automation Engineer",
navText: "Team Lead", placeholderEntry: true,
path: "team-lead",
}, },
{ {
enabled: false, enabled: false,
navText: "Zebrafish Embryo Pick and Plate", navText: "Zebrafish Embryo Pick and Plate",
isSubItem: true,
path: "zebrafish-embryo-pick-and-plate", path: "zebrafish-embryo-pick-and-plate",
}, },
{ {
enabled: false, enabled: false,
navText: "Shuttlebox Behavior System", navText: "Shuttlebox Behavior System",
isSubItem: true,
path: "shuttlebox-behavior-system", path: "shuttlebox-behavior-system",
}, },
{ {
enabled: false,
navText: "Dechorionator", navText: "Dechorionator",
isSubItem: true,
path: "dechorionator", path: "dechorionator",
}, },
{ {
enabled: false, enabled: false,
navText: "Denso Embryo Pick and Plate", navText: "Denso Embryo Pick and Plate",
isSubItem: true,
path: "denso-embryo-pick-and-plate", path: "denso-embryo-pick-and-plate",
}, },
{ {
enabled: false, enabled: false,
navText: "ZScan Processor", navText: "ZScan Processor",
isSubItem: true,
path: "zscan-processor", path: "zscan-processor",
}, },
], ],
@@ -262,7 +269,10 @@ export const getPaths = (
]; ];
} else { } else {
let enabled = currentEntry.enabled ?? true; let enabled = currentEntry.enabled ?? true;
if (disabledOnly ? !enabled : enabled) { if (
(disabledOnly ? !enabled : enabled) &&
!currentEntry.placeholderEntry
) {
foundPaths.push("/" + [...paths, currentEntry.path || ""].join("/")); foundPaths.push("/" + [...paths, currentEntry.path || ""].join("/"));
} }
} }

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

@@ -0,0 +1,32 @@
import type { ImageMetadata } from "astro";
import { DateTime } from "luxon";
import type { timelineEntry } from "@interfaces/timeline.ts";
import type { lineItem } from "@interfaces/ul-li.ts";
export interface printedCircuitBoardRevision {
major: number;
minor: number;
patch: number;
date: DateTime;
images?: ImageMetadata[];
notes?: lineItem[];
}
export interface printedCircuitBoard {
name: string;
description: string;
revisions: printedCircuitBoardRevision[];
}
export const timelineFromPrintedCircuitBoard = (
pcb: printedCircuitBoard,
): timelineEntry[] =>
pcb.revisions?.map((revision) => ({
event: `PCB Released: ${pcb.name} `,
eventDetail: `Revision: ${revision.major}.${revision.minor}.${revision.patch}`,
date: revision.date,
}));

View File

@@ -1,8 +1,13 @@
export interface navLink { export interface navLink {
enabled?: boolean; enabled?: boolean;
hidden?: boolean; hidden?: boolean;
placeholderEntry?: boolean;
navText: string; navText: string;
isSubItem?: boolean; // For visual distinction only
path?: string; path?: string;
pubpath?: string; pubpath?: string;
children?: navLink[]; children?: navLink[];
} }

View File

@@ -1,6 +1,8 @@
import { DateTime } from "luxon";
export interface timelineEntry { export interface timelineEntry {
event: string; event: string;
eventDetail?: string; eventDetail?: string;
date: string; date: DateTime;
description?: string; description?: string;
} }

View File

@@ -46,7 +46,7 @@ const pageEnabled = pathToMetadata(Astro.url.pathname).enabled ?? true;
class="grow overflow-x-hidden overflow-y-scroll" class="grow overflow-x-hidden overflow-y-scroll"
> >
<Navbar /> <Navbar />
<main class="mx-6 my-4"> <main class="mx-6 my-4 space-y-4">
{ {
showTitle && pageEnabled && ( showTitle && pageEnabled && (
<PageGroup> <PageGroup>

View File

@@ -1,4 +1,6 @@
--- ---
import { DateTime } from "luxon";
import BaseLayout from "@layouts/BaseLayout.astro"; import BaseLayout from "@layouts/BaseLayout.astro";
import H2 from "@components/H2.astro"; import H2 from "@components/H2.astro";
@@ -25,12 +27,12 @@ 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: DateTime.fromISO("2011-06-15"),
}, },
{ {
event: "B.S. Computer Science", event: "B.S. Computer Science",
eventDetail: "Oregon State University", eventDetail: "Oregon State University",
date: "June 2019", date: DateTime.fromISO("2019-06-15"),
}, },
]; ];

View File

@@ -1,25 +1,27 @@
import { DateTime } from "luxon";
import type { timelineEntry } from "@interfaces/timeline.ts"; import type { timelineEntry } from "@interfaces/timeline.ts";
export const deploymentTimeline: timelineEntry[] = [ export const deploymentTimeline: timelineEntry[] = [
{ {
event: "Setup & Ocean Trials", event: "Setup & Ocean Trials",
eventDetail: "Petersburg, AK", eventDetail: "Petersburg, AK",
date: "April 2017", date: DateTime.fromISO("2017-04-01"),
}, },
{ {
event: "Glacier Deployment #1", event: "Glacier Deployment #1",
eventDetail: "LeConte Glacier, AK", eventDetail: "LeConte Glacier, AK",
date: "May 2017", date: DateTime.fromISO("2017-05-01"),
}, },
{ {
event: "Glacier Deployment #2", event: "Glacier Deployment #2",
eventDetail: "LeConte Glacier, AK", eventDetail: "LeConte Glacier, AK",
date: "September 2017", date: DateTime.fromISO("2017-09-01"),
}, },
{ {
event: "Scientific Paper Published", event: "Scientific Paper Published",
eventDetail: "The Oceanographic Society", eventDetail: "The Oceanographic Society",
date: "September 2017", date: DateTime.fromISO("2017-09-02"),
}, },
]; ];

View File

@@ -30,6 +30,7 @@ import publication from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic
import ross_team from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler/ross-team.jpg"; import ross_team from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler/ross-team.jpg";
import ui from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler/ui.jpg"; import ui from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler/ui.jpg";
import { DateTime } from "luxon";
import { import {
deploymentTimeline, deploymentTimeline,
subTitles, subTitles,
@@ -53,13 +54,13 @@ const timeline: timelineEntry[] = [
{ {
event: "Started", event: "Started",
eventDetail: "Joined ROSS", eventDetail: "Joined ROSS",
date: "April 2016", date: DateTime.fromISO("2016-04-01"),
}, },
...deploymentTimeline, ...deploymentTimeline,
{ {
event: "Finished", event: "Finished",
eventDetail: "Left ROSS", eventDetail: "Left ROSS",
date: "May 2018", date: DateTime.fromISO("2018-05-01"),
}, },
]; ];

View File

@@ -1,11 +1,15 @@
--- ---
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
import H2 from "@components/H2.astro"; import H2 from "@components/H2.astro";
import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro"; import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import YtVideo from "@components/Media/YtVideo.astro"; import YtVideo from "@components/Media/YtVideo.astro";
import PageGroup from "@components/PageGroup.astro"; import PageGroup from "@components/PageGroup.astro";
import type { carouselGroup } from "@interfaces/image-carousel.ts"; import type { carouselGroup } from "@interfaces/image-carousel.ts";
import type { videoConfig } from "@interfaces/yt-video.ts"; import type { videoConfig } from "@interfaces/video.ts";
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
import { roverSubTitles } from "./osu-robotics-club.ts";
import circ_champions from "@assets/experience/osu-robotics-club/mars-rover-software-lead/circ-champions.jpg"; import circ_champions from "@assets/experience/osu-robotics-club/mars-rover-software-lead/circ-champions.jpg";
import corwin_at_competition from "@assets/experience/osu-robotics-club/mars-rover-software-lead/corwin-at-competition.jpg"; import corwin_at_competition from "@assets/experience/osu-robotics-club/mars-rover-software-lead/corwin-at-competition.jpg";
@@ -47,7 +51,7 @@ const videos: videoConfig[] = [
]; ];
--- ---
<ExperienceLayout title="OSURC - Software Team Lead"> <ExperienceLayout title="Software Team Lead" subTitles={roverSubTitles}>
<Carousel carouselGroup={headerCarouselGroup} /> <Carousel carouselGroup={headerCarouselGroup} />
<PageGroup> <PageGroup>

View File

@@ -0,0 +1,3 @@
export const subTitles = ["Oregon State University", "OSU Robotics Club"];
export const roverSubTitles = [...subTitles, "Mars Rover Team"];

View File

@@ -1,5 +1,235 @@
--- ---
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
import H2 from "@components/H2.astro";
import H3 from "@components/H3.astro";
import Li from "@components/Li.astro";
import CustomCarousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import PageGroup from "@components/PageGroup.astro";
import Paragraph from "@components/Paragraph.astro";
import Paragraphs from "@components/Paragraphs.astro";
import PrintedCircuitBoard from "@components/PrintedCircuitBoard.astro";
import SkillMatrix from "@components/SkillMatrix/SkillMatrix.astro";
import Timeline from "@components/Timeline/Timeline.astro";
import Ul from "@components/Ul.astro";
import type { carouselGroup } from "@interfaces/image-carousel.ts";
import { timelineFromPrintedCircuitBoard } from "@interfaces/printed-circuit-board.ts";
import type { categorySkills } from "@interfaces/skill-matrix.ts";
import type { timelineEntry } from "@interfaces/timeline.ts";
import { dechorionatorPcb } from "./dechorionator.ts";
import {
subTitles,
workingTimeline,
} from "./osu-sinnhuber-aquatic-research-laboratory.ts";
import assembly_internal_overview from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/assembly-internal-overview.jpg";
import assembly_pcb_connected from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/assembly-pcb-connected.jpg";
import assembly_pcb_front_on from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/assembly-pcb-front-on.jpg";
import assembly_pcb_front from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/assembly-pcb-front.jpg";
import assembly_pcb_rear from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/assembly-pcb-rear.jpg";
import assembly_surround_front from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/assembly-surround-front.jpg";
import assembly_surround_rear from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/assembly-surround-rear.jpg";
import off_front_lid_open from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/off-front-lid-open.jpg";
import off_front from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/off-front.jpg";
import on_front_closeup from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/on-front-closeup.jpg";
import on_front from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/on-front.jpg";
import rear_water_manifold from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/rear-water-manifold.jpg";
import rear from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/rear.jpg";
import top_drain from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/top-drain.jpg";
import top_holder_closeup from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/top-holder-closeup.jpg";
import top_lid_open from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/top-lid-open.jpg";
import top_showerhead from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/top-showerhead.jpg";
import travel_setup from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/travel-setup.jpg";
import InlineLink from "@components/InlineLink.astro";
import PopoverWordDefinition from "@components/PopoverWordDefinition.astro";
const headerCarouselGroup: carouselGroup = {
animation: "slide",
images: [
off_front,
off_front_lid_open,
on_front,
on_front_closeup,
top_lid_open,
top_showerhead,
top_drain,
top_holder_closeup,
rear,
rear_water_manifold,
travel_setup,
assembly_surround_front,
assembly_surround_rear,
assembly_pcb_rear,
assembly_pcb_front,
assembly_pcb_front_on,
assembly_pcb_connected,
assembly_internal_overview,
],
};
const timeline: timelineEntry[] = [
...workingTimeline,
...timelineFromPrintedCircuitBoard(dechorionatorPcb),
];
const categorizedSkills: categorySkills[] = [
{
category: "Electrical",
skills: [
{
item: "Schematic & PCB Design",
subItems: [
{ item: "Mentor Graphics PADS" },
{ item: "Altium Designer" },
],
},
{
item: "PCB Assembly & Rework",
subItems: [
{ item: "Handheld Soldering" },
{ item: "Handheld Hot-Air Reflow" },
{ item: "Oven Reflow" },
],
},
{
item: "Electrical Diagnostics",
subItems: [{ item: "Multimeters" }, { item: "Oscilloscopes" }],
},
],
},
{
category: "Software & Environments",
skills: [
{ item: "Git" },
{
item: "Programming",
subItems: [{ item: "Low-Level Embedded C/C++ (Atmel Studio)" }],
},
],
},
];
--- ---
<ExperienceLayout title="SARL - Dechorionator" /> <ExperienceLayout title="Dechorionator" subTitles={subTitles}>
<CustomCarousel carouselGroup={headerCarouselGroup} />
<PageGroup>
<Fragment slot="header"><H2>Summary</H2></Fragment>
<PageGroup>
<Fragment slot="header"><H3>Timeline</H3></Fragment>
<Timeline timeline={timeline} />
</PageGroup>
<PageGroup>
<Fragment slot="header"><H3>Key Takeaways</H3></Fragment>
<Ul>
<Li
>Created an all-in-one tool for removing the chorions of zebrafish
embryos in a controlled and repeatable manner</Li
>
<Li
>Developed custom PCBs to handle motion, pump control, and user
interaction</Li
>
<Li
>Deployed multiple units to the lab, and one to an east-coast partner
laboratory</Li
>
<Li
>Cost reduced to roughly 1/5 that of the lab's previous dechorionation
hardware</Li
>
</Ul>
</PageGroup>
<SkillMatrix categorizedSkills={categorizedSkills} />
</PageGroup>
<PageGroup>
<Fragment slot="header"><H2>Details</H2></Fragment>
<Paragraphs>
<Paragraph>
Before delving into what was built, some quick context is probably
needed. A dechorionator is a device that removes <InlineLink
href="https://en.wikipedia.org/wiki/Chorion">chorions</InlineLink
> from embryos. Chorions are the outer membranes of an embryo which provide
protection, and a permeable membrane which can allow gasses and nutrients
to reach the developing animal inside. As SARL is a toxicology lab, and its
experiments need to be deterministic, this protective layer can drastically
skew tests results, and even worse, can can variances embryo to embryo, or
egg batch to egg batch. To remove this, a special protein is added to a petri-dish
full of embryos, and then the dish is gently swirled with jerking start and
stop motions. The goal is to provide light agitation between the embryo, the
dish, and their neighbors, helping the protein eat away the chorion and sluff
off into the dish. This can, and has been done by hand, but when I joined
SARL they already had two machines which which could automatically perform
this task. However, they were incredibly expensive and massively overcomplicated,
requiring a whole table's worth of custom shaker units, networked peristaltic
pumps, and servos. The engineering team was tasked with simplifying this setup
while reducing both their size and cost.
</Paragraph>
<Paragraph>
We started with a <PopoverWordDefinition key="COTS" /> shaker unit from the
company ELMI, which had a stepper-motor-based drive system, making it a perfect
candidate for easy retrofit. After gutting the existing electronics, and taking
some measurements, I started on a custom PCB design. Basic requirements were
that the board needed to be able to control the stepper motor, control the
speed of a liquid pump, provide controls to users, allow for config editing
from those controls, and provide a screen for cycle progress and editing those
config values. Since this was one of the first PCBs I'd ever designed and
hand-assembled, I started with a basic proof-of-concept which was for bench
use only (revision: 1.1.0). While I worked on the electronics, my co-worker
and good friend <InlineLink href="https://dylanthrush.com"
>Dylan Thrush</InlineLink
> was busy designing a top-plate for the shaker to hold the dishes, shower
them with water, and drain the pumped-in liquid.
</Paragraph>
<Paragraph>
First tests showed that the overall concept was going to work, just
needing signal conditioning for the rotary encoder to avoid ghosted or
missing inputs. A larger problem we found was that the brushed-dc-motor
driven peristaltic pump was not going to be able to supply the flowrate
needed to properly shower the four dishes. We'd already chosen one of
the highest-flowrate pumps which could fit inside the shaker housing,
and ended up having to pivot to a much more expensive one from TCS
Micropumps. Luckily, not only did it solve our flowrate problem, but
also held up much better to the saltwater solution being pumped through
it than our initial choice.
</Paragraph>
<Paragraph>
With the proof-of-concept design functional, I began a redesign of the
control PCB to replace the existing control panel and drive circuitry
from the ELMI shaker (revision: 3.0.0). The existing control circuitry
had a unique assembly design that I'd not encountered before, using a
PCB with solderable standoffs as the front panel, and soldered copper
strips as retention tabs. I was so fascinated by the design that I
decided to emulate it. Check out the images at the end of the reel above
to see how this unique assembly was put together! Around the time that
the PCBs were ready, we'd hired a new engineer, <InlineLink
href="https://www.linkedin.com/in/aaron-rito-2b754777/"
>Aaron Rito</InlineLink
>, who I tasked with writing the firmware while providing input and
guidance.
</Paragraph>
<Paragraph>
Over the next few months, many revisions were made to the firmware, as
well as mechanical designs for the showerheads and water manifold. Dylan
also had a final design for the top-plate milled out that looked
beautiful. Once those changes were complete, we provided the prototype
unit to the researchers, along with documentation on how to use the
tuning values. They then spent a few weeks running the new dechorionator
alongside the old ones, while tweaking these parameters until the
performance matched. We then built up four more units, and pre-flashed
them with this configuration. Three of these went into the lab, where a
total of four of our new dechorionators sat on the same table where just
two prior-generation ones used to live. It even had additional space for
pre and post prep work on the petri-dishes! The last one I installed in
a partner lab on the east coast after flying there with the head of the
lab, Robyn Tanguay and deputy director, Lisa Truong.
</Paragraph>
</Paragraphs>
</PageGroup>
<PageGroup>
<Fragment slot="header"><H2>Printed Circuit Boards</H2></Fragment>
<PrintedCircuitBoard pcb={dechorionatorPcb} />
</PageGroup>
</ExperienceLayout>

View File

@@ -0,0 +1,56 @@
import { DateTime } from "luxon";
import type { printedCircuitBoard } from "@interfaces/printed-circuit-board.ts";
import pcb_1_1_0_assembled_bottom_angle from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/1-1-0/assembled-bottom-angle.jpg";
import pcb_1_1_0_assembled_bottom from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/1-1-0/assembled-bottom.jpg";
import pcb_1_1_0_assembled_side from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/1-1-0/assembled-side.jpg";
import pcb_1_1_0_bare_bottom from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/1-1-0/bare-bottom.png";
import pcb_1_1_0_bare_top from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/1-1-0/bare-top.png";
import assembled_main_bottom from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/assembled-main-bottom.jpg";
import assembled_side from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/assembled-side.jpg";
import bare_main_bottom from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/bare-main-bottom.jpg";
import bare_main_top from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/bare-main-top.jpg";
import bare_surround_bottom from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/bare-surround-bottom.jpg";
import bare_surround_top from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/bare-surround-top.jpg";
import half_assembled_main_top_surround_bottom_lcd from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/half-assembled-main-top-surround-bottom-lcd.jpg";
import half_assembled_main_top_surround_bottom from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/half-assembled-main-top-surround-bottom.jpg";
export const dechorionatorPcb: printedCircuitBoard = {
name: "Dechorionator",
description:
"Control board which provides motion and water flow control, along with user control and monitoring.",
revisions: [
{
major: 3,
minor: 0,
patch: 0,
date: DateTime.fromISO("2015-08-30"),
images: [
half_assembled_main_top_surround_bottom,
half_assembled_main_top_surround_bottom_lcd,
assembled_main_bottom,
assembled_side,
bare_main_top,
bare_main_bottom,
bare_surround_top,
bare_surround_bottom,
],
},
{
major: 1,
minor: 1,
patch: 0,
date: DateTime.fromISO("2014-05-10"),
images: [
pcb_1_1_0_assembled_bottom_angle,
pcb_1_1_0_assembled_bottom,
pcb_1_1_0_assembled_side,
pcb_1_1_0_bare_top,
pcb_1_1_0_bare_bottom,
],
// notes: [{ item: "Effective first functional revision" }],
},
],
};

View File

@@ -0,0 +1,23 @@
import { DateTime } from "luxon";
import type { timelineEntry } from "@interfaces/timeline.ts";
export const subTitles = [
"Oregon State University",
"College of Agricultural Sciences",
"Department of Environmental and Molecular Toxicology",
"Sinnhuber Aquatic Research Laboratory",
];
export const workingTimeline: timelineEntry[] = [
{
event: "Started",
eventDetail: "Joined SARL Engineering",
date: DateTime.fromISO("2013-09-01"),
},
{
event: "Finished",
eventDetail: "Left SARL Engineering",
date: DateTime.fromISO("2019-08-01"),
},
];

View File

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

View File

@@ -19,6 +19,7 @@ import swag from "@assets/experience/spacex/avionics-test-engineering-internship
import type { carouselGroup } from "@interfaces/image-carousel.ts"; import type { carouselGroup } from "@interfaces/image-carousel.ts";
import type { categorySkills } from "@interfaces/skill-matrix.ts"; import type { categorySkills } from "@interfaces/skill-matrix.ts";
import type { timelineEntry } from "@interfaces/timeline.ts"; import type { timelineEntry } from "@interfaces/timeline.ts";
import { DateTime } from "luxon";
const headerCarouselGroup: carouselGroup = { const headerCarouselGroup: carouselGroup = {
animation: "slide", animation: "slide",
@@ -28,11 +29,11 @@ const headerCarouselGroup: carouselGroup = {
const timeline: timelineEntry[] = [ const timeline: timelineEntry[] = [
{ {
event: "Started", event: "Started",
date: "January 2019", date: DateTime.fromISO("2019-01-01"),
}, },
{ {
event: "Finished", event: "Finished",
date: "March 2019", date: DateTime.fromISO("2019-03-01"),
}, },
]; ];

View File

@@ -20,6 +20,7 @@ import type { timelineEntry } from "@interfaces/timeline.ts";
import five_year_patch from "@assets/experience/spacex/hardware-test-engineer-i-ii/five-year-patch.jpg"; import five_year_patch from "@assets/experience/spacex/hardware-test-engineer-i-ii/five-year-patch.jpg";
import starlink_headquarters_selfie from "@assets/experience/spacex/hardware-test-engineer-i-ii/starlink-headquarters-selfie.jpg"; import starlink_headquarters_selfie from "@assets/experience/spacex/hardware-test-engineer-i-ii/starlink-headquarters-selfie.jpg";
import starlink_patch from "@assets/experience/spacex/hardware-test-engineer-i-ii/starlink-patch.jpg"; import starlink_patch from "@assets/experience/spacex/hardware-test-engineer-i-ii/starlink-patch.jpg";
import { DateTime } from "luxon";
const headerCarouselGroup: carouselGroup = { const headerCarouselGroup: carouselGroup = {
animation: "slide", animation: "slide",
@@ -30,28 +31,28 @@ const timeline: timelineEntry[] = [
{ {
event: "Started", event: "Started",
eventDetail: "Satellite Hardware Test Team", eventDetail: "Satellite Hardware Test Team",
date: "September 2019", date: DateTime.fromISO("2019-09-01"),
description: description:
"Owned test systems for four generations of Starlink flight computers and two generations of power boards", "Owned test systems for four generations of Starlink flight computers and two generations of power boards",
}, },
{ {
event: "Transitioned To Remote", event: "Transitioned To Remote",
eventDetail: "Moved To Oregon", eventDetail: "Moved To Oregon",
date: "August 2022", date: DateTime.fromISO("2022-08-01"),
description: description:
"Personal decision, but I was allowed to work on tools for the build reliability engineering team", "Personal decision, but I was allowed to work on tools for the build reliability engineering team",
}, },
{ {
event: "Changed Teams", event: "Changed Teams",
eventDetail: "Components Test Infra Team", eventDetail: "Components Test Infra Team",
date: "March 2024", date: DateTime.fromISO("2024-03-01"),
description: description:
"Vertical move that allowed for broader application of my skills", "Vertical move that allowed for broader application of my skills",
}, },
{ {
event: "Finished", event: "Finished",
eventDetail: "Thanks for all the fish!", eventDetail: "Thanks for all the fish!",
date: "April 2025", date: DateTime.fromISO("2025-04-01"),
description: description:
"Celebrated five and a half years of helping put thousands of satellites, and dozens of rockets, into orbit", "Celebrated five and a half years of helping put thousands of satellites, and dozens of rockets, into orbit",
}, },

View File

@@ -103,16 +103,16 @@ const categorizedSkills: categorySkills[] = [
From the DevOps perspective, I created a Makefile within the repo for From the DevOps perspective, I created a Makefile within the repo for
local development targets to build, run, and test both in a pure local development targets to build, run, and test both in a pure
context, and within a Docker container. After pushing updates on a context, and within a Docker container. After pushing updates on a
branch to my local Gitea instance, and opening a pull request, the branch to my local Gitea instance, and opening a pull request, an
website performs spelling checks, runs unit tests, and integration actions runner performs spelling checks, runs unit tests, and
tests. Once these pass, the website is built into a Docker container and integration tests. Once these pass, the website is built into a Docker
uploaded to the registry in my Gitea instance. The image is then container and uploaded to the registry in my Gitea instance. The image
deployed to staging on my <PopoverWordDefinition key="VPS" />, allowing is then deployed to staging on my <PopoverWordDefinition key="VPS" />,
for manual validation of the changes. In order to merge, the build, allowing for manual validation of the changes. In order to merge, the
test, and deploy actions must pass, and ideally I should be empirically build, test, and deploy actions must pass, and I empirically validate
validating the staging deployment. After merging and closing the pull the deployment. Post-merge, a similar action builds, tests, and deploys
request, another action builds, tests, and deploys the main branch in the main branch in the same way as before, but to production, and is
the same way as before, but to production, and is what you see here! what you see here!
</Paragraph> </Paragraph>
</Paragraphs> </Paragraphs>
<SkillMatrix categorizedSkills={categorizedSkills} /> <SkillMatrix categorizedSkills={categorizedSkills} />

View File

@@ -10,9 +10,9 @@ import Paragraphs from "@components/Paragraphs.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 headshot from "@assets/about/headshot.jpg"; import headshot from "@assets/about/headshot.jpg";
import circ_champions from "@assets/experience/osu-robotics-club/mars-rover-software-lead/circ-champions.jpg"; import circ_champions from "@assets/experience/osu-robotics-club/mars-rover-software-lead/circ-champions.jpg";
import alaska_bike_mountain_ocean from "@assets/hobby/motorcycling/trips/2025-08-alaska/alaska-bike-mountain-ocean.jpg";
const headerCarouselGroup: carouselGroup = { const headerCarouselGroup: carouselGroup = {
animation: "slide", animation: "slide",
@@ -88,7 +88,7 @@ const headerCarouselGroup: carouselGroup = {
>rfid implant</InlineLink >rfid implant</InlineLink
> in my hand! > in my hand!
</Paragraph> </Paragraph>
<Paragraph class="mt-8 flex flex-col items-center" initialTab={false}> <Paragraph class="mt-4 flex flex-col items-center" initialTab={false}>
<div> <div>
If you're interested in contacting me, feel free to message on <InlineLink If you're interested in contacting me, feel free to message on <InlineLink
href="https://github.com/caperren">LinkedIn</InlineLink href="https://github.com/caperren">LinkedIn</InlineLink

View File

@@ -4,4 +4,8 @@ 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="Hardware Test Engineer"
subTitles={["2019-07-01"]}
resume={resume}
/>

View File

@@ -4,4 +4,8 @@ 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="Infrastructure Engineer"
subTitles={["2025-10-27"]}
resume={resume}
/>

View File

@@ -40,5 +40,8 @@ test("Pages Missing from Astro Paths", () => {
siteLayoutPaths, siteLayoutPaths,
astroStaticPaths, astroStaticPaths,
); );
expect(siteLayoutNotAstroPaths).toHaveLength(0); expect(
siteLayoutNotAstroPaths,
`FOUND: ${[...siteLayoutNotAstroPaths]}`,
).toHaveLength(0);
}); });