Made a baseline working carousel, timeline, and started flushing out content for primary spacex experience

This commit is contained in:
2025-11-06 01:21:27 -08:00
parent 6f728ad146
commit d6e75ae2ea
26 changed files with 638 additions and 100 deletions

View File

@@ -1,8 +0,0 @@
---
const { images } = Astro.props;
---
<div class="carousel">
{images.map(img => (
<img src={img} alt="carousel item" style="width:100%; max-width:600px; margin: 1rem auto; display:block;" />
))}
</div>

View File

@@ -0,0 +1,173 @@
---
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}>
<!-- Carousel wrapper -->
<div class="relative overflow-hidden h-56 md:h-156">
{
groupToShow.images.map((image, index) => (
<div class="hidden duration-700 ease-in-out" data-custom-carousel-item>
<Image src={image}
class="relative sm:max-w-xl md:max-w-3xl lg:max-w-5xl xl:max-w-7xl -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2"
alt="..."
loading="eager"/>
</div>
))
}
</div>
{(groupToShow.images.length > 1) && (
<!-- Slider indicators -->
<div class="absolute z-30 flex -translate-x-1/2 bottom-8 md: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"
fill="none"
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"
fill="none"
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>
import {Carousel, type CarouselItem, type CarouselOptions, type IndicatorItem} from 'flowbite';
class CustomCarousel extends HTMLElement {
_slide: boolean;
_items: CarouselItem[];
_options: CarouselOptions;
_carousel: Carousel;
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._carousel.cycle();
this._attachHandlers()
}
_getItems = (): CarouselItem[] => {
let customItems = this.querySelectorAll('[data-custom-carousel-item]') || [];
return Array.from(customItems).map(
(item, index): CarouselItem => {
return {el: item as HTMLElement, position: index}
}
)
}
_getOptions = (): CarouselOptions => {
let customIndicators = this.querySelectorAll('[data-custom-carousel-slide-to]') || [];
return {
defaultPosition: 1,
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 => {
// Controls
const carouselNextEl = this.querySelector(
'[data-custom-carousel-next]'
);
const carouselPrevEl = this.querySelector(
'[data-custom-carousel-prev]'
);
if (carouselNextEl) {
carouselNextEl.addEventListener('click', () => {
this._carousel.next();
});
}
if (carouselPrevEl) {
carouselPrevEl.addEventListener('click', () => {
this._carousel.prev();
});
}
}
}
customElements.define('custom-carousel', CustomCarousel)
</script>

View File

@@ -20,7 +20,7 @@ import {siteLayout} from "@data/site-layout.ts";
d="M1 1h15M1 7h15M1 13h15"/>
</svg>
</button>
<div class="hidden w-full md:block md:w-auto" id="navbar-multi-level">
<div class="z-40 hidden w-full md:block md:w-auto" id="navbar-multi-level">
<NestedNavbarEntry items={siteLayout}/>
</div>
</div>

View File

@@ -21,7 +21,7 @@ const getHrefPath = (entry: navLink): string => {
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 md:hover:bg-transparent md:border-0 md:hover:text-caperren-green-light md:p-0 ">
{entry.title}
{entry.navText}
<svg class="w-2.5 h-2.5 ms-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
@@ -39,7 +39,7 @@ const getHrefPath = (entry: navLink): string => {
<a href={getHrefPath(entry)}
class="block py-2 px-3 bg-transparent hover:text-caperren-green-light ring-caperren-green-dark md:p-0"
aria-current="page">{entry.title}</a>
aria-current="page">{entry.navText}</a>
)}
</li>

View File

@@ -0,0 +1,78 @@
---
import type {timelineEntry} from "@interfaces/timeline.ts";
const timeline: timelineEntry[] = Astro.props.timeline || [];
---
<custom-timeline>
<div class="relative z-10 grid gap-6 grid-flow-row sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 3xl:grid-cols-6"
data-timeline>
{timeline.map((entry, index) => (
<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}
>
<h3 class="text-lg font-bold">
{entry.event}
</h3>
<h4 class="font-semibold leading-none">
{entry.eventDetail}
</h4>
<time class="mb-2 mt-1 text-sm italic leading-none">
{entry.date}
</time>
{entry.description && (
<p class="text-sm font-normal">
{entry.description}
</p>
)}
</div>
))}
</div>
</custom-timeline>
<script>
import LeaderLine from "leader-line-new";
class CustomTimeline extends HTMLElement {
_eventElements: Element[];
constructor() {
super();
this._eventElements = this._getNodeElements();
this._paintLeaderLines();
}
_getNodeElements = (): Element[] => {
return Array.from(
this.querySelectorAll('[data-timeline-node-index]')
).sort(
(elementA, elementB) =>
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)
pairs.forEach(pair => {
new LeaderLine({
start: pair[0],
end: pair[1],
color: '#10ac25',
size: 3,
startSocket: "right",
endSocket: "left",
startPlug: "square",
endPlug: "arrow3"
});
});
}
}
customElements.define('custom-timeline', CustomTimeline)
</script>

View File

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

9
src/env.d.ts vendored
View File

@@ -6,4 +6,11 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}
}
declare global {
interface Window {
LeaderLine: any;
}
}
export {};

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
export interface navLink {
title: string;
navText: string;
path?: string;
pubpath?: string;
children?: navLink[];

View File

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

View File

@@ -1,7 +1,8 @@
---
import '../styles/global.css'
import Navbar from '../components/Navbar.astro';
import Footer from '../components/Footer.astro';
import '@styles/global.css'
import Navbar from '@components/Navbar.astro';
import Footer from '@components/Footer.astro';
const pageTitle = Astro.props.title ? `${Astro.props.title} - Corwin Perren` : "Corwin Perren";
---
@@ -12,14 +13,19 @@ const pageTitle = Astro.props.title ? `${Astro.props.title} - Corwin Perren` : "
<link rel="icon" href="/favicon.svg" type="image/svg+xml"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{pageTitle}</title>
<script is:inline type="module" src="/src/scripts/main.js"></script>
</head>
<body class="bg-black text-white">
<Navbar/>
<main class="mx-6 mt-6 mb-14">
{(Astro.props.title) && (
<h1 class="font-extrabold md:text-3xl md:mb-6">{Astro.props.title}</h1>
)}
<slot/>
</main>
<Footer/>
</body>
</html>
<script is:inline type="module" src="/src/scripts/main.js"></script>

View File

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

View File

@@ -1,11 +1,60 @@
---
import ExperienceLayout from '@layouts/ExperienceLayout.astro';
import {Image} from 'astro:assets';
import Timeline from '@components/Timeline.astro';
import Carousel from "@components/CustomCarousel.astro";
import spring_2019_interns from "@assets/experience/spacex/avionics-test-engineering-internship/spring-2019-interns.jpg";
---
<ExperienceLayout>
<Image class="mx-auto block" src={spring_2019_interns} alt="spring-2019-interns.jpg" loading="eager"/>
<span>Final text here</span>
import type {carouselGroup} from "@interfaces/image-carousel.ts";
import type {timelineEntry} from "@interfaces/timeline.ts";
const headerCarouselGroup: carouselGroup = {
animation: "slide",
images: [
spring_2019_interns
]
}
const timeline: timelineEntry[] = [
{
event: "Started",
date: "January 2019",
},
{
event: "Finished",
date: "March 2019",
}
];
---
<ExperienceLayout title="SpaceX - Avionics Test Engineering Internship">
<!-- FIXME: Image bounds are all messed up -->
<Carousel carouselGroup={headerCarouselGroup}/>
<h2 class="font-bold md:text-2xl my-4">Summary</h2>
<h3 class="font-bold md:text-lg my-4">Timeline</h3>
<Timeline timeline={timeline}/>
<h3 class="font-bold md:text-lg my-4">Key Takeaways</h3>
<ul class="list-disc list-inside">
<li></li>
</ul>
<h3 class="font-bold md:text-lg my-4">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="font-extrabold text-sm">Software</div>
<hr class="text-caperren-green"/>
<ul class="list-disc list-inside text-sm">
<li>Python</li>
<li>Test Driven Development</li>
</ul>
</div>
<div>
<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>
<h2 class="font-bold md:text-2xl my-4">Details</h2>
</ExperienceLayout>

View File

@@ -1,13 +1,87 @@
---
import ExperienceLayout from '@layouts/ExperienceLayout.astro';
import {Image} from 'astro:assets';
import Timeline from '@components/Timeline.astro';
import Carousel from "@components/CustomCarousel.astro";
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 type {carouselGroup} from "@interfaces/image-carousel.ts";
import type {timelineEntry} from "@interfaces/timeline.ts";
const headerCarouselGroup: carouselGroup = {
animation: "slide",
images: [
starlink_headquarters_selfie
]
}
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="SpaceX - Hardware Test Engineer II" >
<Image class="mx-auto block" src={starlink_headquarters_selfie} alt="starlink_headquarters_selfie" loading="eager"/>
<h2 >Timeline</h2>
<ul>
<li>Test</li>
<ExperienceLayout title="SpaceX - Hardware Test Engineer I/II">
<Carousel carouselGroup={headerCarouselGroup}/>
<h2 class="font-bold md:text-2xl my-4">Summary</h2>
<h3 class="font-bold md:text-lg my-4">Timeline</h3>
<Timeline timeline={timeline}/>
<h3 class="font-bold md:text-lg my-4">Key Takeaways</h3>
<ul class="list-disc list-inside">
<li>Created test systems which validated ~4500 Starlink satellite flight computers, and ~4000 power boards</li>
<li>Developed program-critical infrastructure that enabled efficient triage, management, and tracking of hardware failures</li>
<li>Designed and deployed automated, unified, and containerized infrastructure to greatly increase application reliability and development speed </li>
</ul>
<h3 class="font-bold md:text-lg my-4">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="font-extrabold text-sm">Software</div>
<hr class="text-caperren-green"/>
<ul class="list-disc list-inside text-sm">
<li>Python</li>
<li>Test Driven Development</li>
</ul>
</div>
<div>
<div class="font-extrabold text-sm">Electrical</div>
<hr class="text-caperren-green"/>
</div>
<div>
<div class="font-extrabold text-sm">Mechanical</div>
<hr class="text-caperren-green"/>
</div>
<div>
<div class="font-extrabold text-sm">Other</div>
<hr class="text-caperren-green"/>
</div>
</div>
<h2 class="font-bold md:text-2xl my-4">Details By Team</h2>
<h3 class="font-bold md:text-lg my-4">Starlink Hardware Test</h3>
<h3 class="font-bold md:text-lg my-4">Build Reliability Engineering</h3>
<h3 class="font-bold md:text-lg my-4">Components Test Infrastructure</h3>
</ExperienceLayout>

View File

@@ -1,9 +1,20 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import {Image} from 'astro:assets';
import Carousel from "@components/CustomCarousel.astro";
import headshot from "../assets/headshot.png";
import headshot from "@assets/headshot.png";
import type {carouselGroup} from "@interfaces/image-carousel.ts";
const headerCarouselGroup: carouselGroup = {
animation: "slide",
images: [
headshot,
headshot
]
}
---
<BaseLayout>
<Image class="mx-auto block" src={headshot} alt="headshot" loading="eager"/>
<Carousel carouselGroup={headerCarouselGroup}/>
</BaseLayout>

View File

@@ -11,3 +11,7 @@
--color-caperren-green-light: #00ff2a;
--color-caperren-green-dark: #06370e;
}
.leader-line {
z-index: 0;
}