Compare commits
23 Commits
main
...
website-co
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cd7e565eb | |||
| cf3b740907 | |||
| 297a56e7d6 | |||
| ee76972667 | |||
| 005e092344 | |||
| ffbc3beaf7 | |||
| 506987c164 | |||
| a807a39b09 | |||
| 40a673a418 | |||
| 9a4ecdd073 | |||
| d24a3054c1 | |||
| d1bc55e556 | |||
| f0319c4446 | |||
| 221587eff9 | |||
| 9ac3295127 | |||
| c5dde92023 | |||
| f91be707d9 | |||
| 7fe9303d5e | |||
| 33388f59ee | |||
| 3e34b94ec5 | |||
| 128dc14459 | |||
| d6e75ae2ea | |||
| 6f728ad146 |
27
.gitea/disabled_workflows/playwright.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Playwright Tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master ]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
timeout-minutes: 60
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: npx playwright install --with-deps
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: npx playwright test
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 30
|
||||||
@@ -22,7 +22,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||||
echo REPO_VERSION_HASH=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
echo REPO_VERSION_HASH=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||||
echo PROJECT_VERSION=$(npm pkg get version --workspaces=false | tr -d \") >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
build_and_push:
|
build_and_push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -54,7 +53,6 @@ jobs:
|
|||||||
gitea.perren.cloud/caperren/caperren-com:latest
|
gitea.perren.cloud/caperren/caperren-com:latest
|
||||||
build-args: |
|
build-args: |
|
||||||
REPO_VERSION_HASH=${{ needs.determine_version.outputs.repo_version_hash }}
|
REPO_VERSION_HASH=${{ needs.determine_version.outputs.repo_version_hash }}
|
||||||
PROJECT_VERSION=${{ needs.determine_version.outputs.project_version }}
|
|
||||||
BUILD_ENVIRONMENT=production
|
BUILD_ENVIRONMENT=production
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||||
echo REPO_VERSION_HASH=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
echo REPO_VERSION_HASH=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||||
echo PROJECT_VERSION=$(npm pkg get version --workspaces=false | tr -d \") >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
build_and_push:
|
build_and_push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -52,7 +51,6 @@ jobs:
|
|||||||
gitea.perren.cloud/caperren/caperren-com:latest-staging
|
gitea.perren.cloud/caperren/caperren-com:latest-staging
|
||||||
build-args: |
|
build-args: |
|
||||||
REPO_VERSION_HASH=${{ needs.determine_version.outputs.repo_version_hash }}
|
REPO_VERSION_HASH=${{ needs.determine_version.outputs.repo_version_hash }}
|
||||||
PROJECT_VERSION=${{ needs.determine_version.outputs.project_version }}
|
|
||||||
BUILD_ENVIRONMENT=staging
|
BUILD_ENVIRONMENT=staging
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
|||||||
7
.gitignore
vendored
@@ -28,3 +28,10 @@ pnpm-debug.log*
|
|||||||
|
|
||||||
# jetbrains setting folder
|
# jetbrains setting folder
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
/playwright/.auth/
|
||||||
|
|||||||
23
Dockerfile
@@ -8,27 +8,26 @@ COPY package.json package-lock.json tsconfig.json astro.config.mjs ./
|
|||||||
FROM base AS prod-deps
|
FROM base AS prod-deps
|
||||||
RUN npm install --omit=dev
|
RUN npm install --omit=dev
|
||||||
|
|
||||||
FROM base AS build-deps
|
FROM prod-deps AS build
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
FROM build-deps AS build
|
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ARG REPO_VERSION_HASH
|
ARG REPO_VERSION_HASH
|
||||||
ARG BUILD_ENVIRONMENT
|
ARG BUILD_ENVIRONMENT
|
||||||
ARG PROJECT_VERSION
|
|
||||||
|
|
||||||
RUN echo "PUBLIC_REPO_VERSION_HASH=\"${REPO_VERSION_HASH}\" \n\
|
RUN echo "PUBLIC_REPO_VERSION_HASH=\"${REPO_VERSION_HASH}\" \n\
|
||||||
PUBLIC_BUILD_ENVIRONMENT=\"${BUILD_ENVIRONMENT}\" \n\
|
PUBLIC_BUILD_ENVIRONMENT=\"${BUILD_ENVIRONMENT}\"" >> .env
|
||||||
PUBLIC_PROJECT_VERSION=\"${PROJECT_VERSION}\"" >> .env
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM httpd:latest AS runtime
|
FROM nginx:alpine AS runtime
|
||||||
WORKDIR /usr/local/apache2/htdocs
|
|
||||||
|
|
||||||
RUN rm index.html
|
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
COPY --from=build /app/dist .
|
RUN chown -R nginx:nginx /usr/share/nginx/html
|
||||||
|
|
||||||
EXPOSE 80
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
6
Makefile
@@ -5,7 +5,8 @@
|
|||||||
fix \
|
fix \
|
||||||
astro_upgrade \
|
astro_upgrade \
|
||||||
build \
|
build \
|
||||||
dev
|
dev \
|
||||||
|
dev-hosted
|
||||||
|
|
||||||
default: dev
|
default: dev
|
||||||
|
|
||||||
@@ -24,3 +25,6 @@ build:
|
|||||||
dev:
|
dev:
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
|
dev-hosted:
|
||||||
|
npm run dev-hosted
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import rehypeAstroRelativeMarkdownLinks from "astro-rehype-relative-markdown-links";
|
|
||||||
|
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
@@ -8,10 +7,6 @@ import tailwindcss from "@tailwindcss/vite";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
integrations: [],
|
integrations: [],
|
||||||
|
|
||||||
markdown: {
|
|
||||||
rehypePlugins: [rehypeAstroRelativeMarkdownLinks],
|
|
||||||
},
|
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
},
|
},
|
||||||
|
|||||||
8
e2e/test-pages-navigable.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('Has Title', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// Expect a title "to contain" a substring.
|
||||||
|
await expect(page).toHaveTitle(/Corwin Perren/);
|
||||||
|
});
|
||||||
14
entrypoint.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Generate a unique token per container launch
|
||||||
|
TOKEN="$(cat /proc/sys/kernel/random/uuid)" # or: TOKEN="$(date +%s%N)"
|
||||||
|
|
||||||
|
# Write it into a file NGINX will include
|
||||||
|
cat >/etc/nginx/conf.d/_release_token.conf <<EOF
|
||||||
|
# auto-generated at container start
|
||||||
|
set \$release_token "$TOKEN";
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Then exec nginx
|
||||||
|
exec nginx -g 'daemon off;'
|
||||||
40
nginx/nginx.conf
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
worker_processes 4;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off;
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
#include /etc/nginx/conf.d/_release_token.conf;
|
||||||
|
|
||||||
|
#etag off;
|
||||||
|
|
||||||
|
#add_header ETag "\"W/$release_token\"" always;
|
||||||
|
#
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
|
error_page 404 /404.html;
|
||||||
|
location = /404.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_cache my_cache;
|
||||||
|
proxy_cache_valid 200 1h;
|
||||||
|
add_header Cache-Control "max-age=0, must-revalidate" always;
|
||||||
|
try_files $uri $uri/index.html =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2724
package-lock.json
generated
20
package.json
@@ -4,15 +4,25 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
|
"dev-hosted": "astro dev --host",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro"
|
"astro": "astro",
|
||||||
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"astro": "^5.15.4",
|
||||||
|
"flowbite": "^3.1.2",
|
||||||
|
"leader-line-new": "^1.1.9",
|
||||||
|
"luxon": "^3.7.2",
|
||||||
|
"tailwindcss": "^4.1.11",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"astro": "^5.11.0",
|
"uuid": "^13.0.0"
|
||||||
"astro-navbar": "^2.4.0",
|
},
|
||||||
"astro-rehype-relative-markdown-links": "^0.18.1",
|
"devDependencies": {
|
||||||
"tailwindcss": "^4.1.11"
|
"@playwright/test": "^1.56.1",
|
||||||
|
"@types/luxon": "^3.7.1",
|
||||||
|
"@types/node": "^24.10.0",
|
||||||
|
"vitest": "^4.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
79
playwright.config.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// import dotenv from 'dotenv';
|
||||||
|
// import path from 'path';
|
||||||
|
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: 'html',
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('')`. */
|
||||||
|
baseURL: 'http://localhost:4321',
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
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 start',
|
||||||
|
// url: 'http://localhost:3000',
|
||||||
|
// reuseExistingServer: !process.env.CI,
|
||||||
|
// },
|
||||||
|
});
|
||||||
BIN
public/favicon-solid.png
Normal file
|
After Width: | Height: | Size: 662 B |
@@ -1,63 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="40mm"
|
|
||||||
height="40mm"
|
|
||||||
viewBox="0 0 40 40"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs1" />
|
|
||||||
<g
|
|
||||||
id="layer1">
|
|
||||||
<rect
|
|
||||||
style="fill:#000000;stroke-width:0.264583"
|
|
||||||
id="rect1"
|
|
||||||
width="58.244999"
|
|
||||||
height="53.954998"
|
|
||||||
x="-6.105"
|
|
||||||
y="-4.2899995" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="layer2">
|
|
||||||
<g
|
|
||||||
id="g1"
|
|
||||||
transform="matrix(3.9840159,0,0,3.9840159,-182.95899,-268.96729)">
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-style:normal;font-weight:normal;font-size:3.38667px;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#10ac25;fill-opacity:1;stroke:none;stroke-width:0.282222px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
x="46.9911"
|
|
||||||
y="76.640976"
|
|
||||||
id="text3344-4-5"><tspan
|
|
||||||
id="tspan3346-7-6"
|
|
||||||
x="46.9911"
|
|
||||||
y="76.640976"
|
|
||||||
style="font-size:11.2889px;line-height:1.25;stroke-width:0.282222px">C</tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-style:normal;font-weight:normal;font-size:3.38667px;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#10ac25;fill-opacity:1;stroke:none;stroke-width:0.282222px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
x="49.597134"
|
|
||||||
y="74.229248"
|
|
||||||
id="text3348-1-8"
|
|
||||||
transform="scale(1.0028223,0.99718564)"><tspan
|
|
||||||
id="tspan3350-3-1"
|
|
||||||
x="49.597134"
|
|
||||||
y="74.229248"
|
|
||||||
style="font-size:4.59543px;line-height:1.25;stroke-width:0.282222px">A</tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.38667px;line-height:0%;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#10ac25;fill-opacity:1;stroke:none;stroke-width:0.282222px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
x="24.869509"
|
|
||||||
y="83.989883"
|
|
||||||
id="text3344-5-7-6"
|
|
||||||
transform="matrix(0.89528488,-0.34221418,0.35348636,0.98184609,0,0)"><tspan
|
|
||||||
id="tspan3370-7-8"
|
|
||||||
x="24.869509"
|
|
||||||
y="83.989883"
|
|
||||||
style="font-size:4.18595px;line-height:1.25;stroke-width:0.282222px">P</tspan></text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB |
BIN
src/assets/about/alaska-bike-mountain-ocean.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
src/assets/about/circ-champions.jpg
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 956 KiB After Width: | Height: | Size: 956 KiB |
BIN
src/assets/education/fhhs-diploma.jpg
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
BIN
src/assets/education/osu-bs-cs-diploma.jpg
Normal file
|
After Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
BIN
src/assets/hobby/body-mods/rfid-implant/injection-site.jpg
Normal file
|
After Width: | Height: | Size: 288 KiB |
BIN
src/assets/hobby/body-mods/rfid-implant/injector-exploded.jpg
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 3.8 MiB |
BIN
src/assets/hobby/body-mods/rfid-implant/xem-pouch.jpg
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
src/assets/hobby/homelab/home-server-rack/rack-from-above.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
src/assets/hobby/homelab/home-server-rack/rack-from-below.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
src/assets/hobby/homelab/home-server-rack/rack-middle.jpg
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
src/assets/hobby/homelab/home-server-rack/rack-top.jpg
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 721 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
BIN
src/assets/hobby/homelab/offsite-backup-rack/enclosure-front.jpg
Normal file
|
After Width: | Height: | Size: 406 KiB |
BIN
src/assets/hobby/homelab/offsite-backup-rack/enclosure-left.jpg
Normal file
|
After Width: | Height: | Size: 344 KiB |
BIN
src/assets/hobby/homelab/offsite-backup-rack/enclosure-rear.jpg
Normal file
|
After Width: | Height: | Size: 382 KiB |
BIN
src/assets/hobby/homelab/offsite-backup-rack/enclosure-right.jpg
Normal file
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 922 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
BIN
src/assets/hobby/homelab/offsite-backup-rack/sata-tight-fit.jpg
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
BIN
src/assets/hobby/homelab/offsite-backup-rack/up-and-running.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
BIN
src/assets/logo-title-large.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
@@ -2,13 +2,14 @@
|
|||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
|
class="logo-title"
|
||||||
width="21.129419mm"
|
width="21.129419mm"
|
||||||
height="9.3295746mm"
|
height="9.3295746mm"
|
||||||
viewBox="0 0 74.868021 33.057548"
|
viewBox="0 0 74.868021 33.057548"
|
||||||
id="svg4376"
|
id="svg4376"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
sodipodi:docname="AP in C Green Mock UP.svg"
|
sodipodi:docname="logo-title.svg"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.6 KiB |
@@ -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>
|
|
||||||
96
src/components/CustomCarousel/CustomCarousel.astro
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
import {Image} from 'astro:assets';
|
||||||
|
|
||||||
|
import type {carouselGroup} from "@interfaces/image-carousel.ts";
|
||||||
|
|
||||||
|
const groupToShow: carouselGroup = Astro.props.carouselGroup;
|
||||||
|
|
||||||
|
const limitByHeightClasses = "sm:max-w-xl md:max-w-3xl lg:max-w-5xl xl:max-w-7xl";
|
||||||
|
const limitByWidthClasses = "max-h-fit";
|
||||||
|
---
|
||||||
|
|
||||||
|
<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 w-full h-56 md:h-120">
|
||||||
|
{
|
||||||
|
groupToShow.images.map((image, index) => (
|
||||||
|
<div class="hidden duration-700 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"
|
||||||
|
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 src="./custom-carousel.ts"/>
|
||||||
80
src/components/CustomCarousel/custom-carousel.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
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._items.length > 0) this._carousel.cycle();
|
||||||
|
|
||||||
|
window.addEventListener("load", 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: 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 => {
|
||||||
|
// 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)
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
---
|
---
|
||||||
<footer class="flex justify-center items-center text-center">
|
|
||||||
{import.meta.env.PUBLIC_BUILD_ENVIRONMENT || "development"} | {import.meta.env.PUBLIC_REPO_VERSION_HASH || "invalid"}@{import.meta.env.PUBLIC_PROJECT_VERSION || "0.0.0"}
|
<footer class="fixed bottom-0 left-0 z-20 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">
|
||||||
</footer>
|
<span>{import.meta.env.PUBLIC_BUILD_ENVIRONMENT || "development"}</span>
|
||||||
|
<span>{import.meta.env.PUBLIC_REPO_VERSION_HASH || "invalid"}</span>
|
||||||
|
</footer>
|
||||||
|
|||||||
@@ -1,150 +1,34 @@
|
|||||||
---
|
---
|
||||||
import {Image} from 'astro:assets';
|
import NestedNavbarEntry from "@components/NestedNavbarEntries.astro";
|
||||||
import {Astronav, MenuItems, MenuIcon, Dropdown, DropdownItems} from "astro-navbar";
|
|
||||||
|
|
||||||
import logoTitle from "../assets/logo-title.svg";
|
import logo_title_large from "@assets/logo-title-large.png";
|
||||||
|
import {siteLayout} from "@data/site-layout.ts";
|
||||||
const navItems = [
|
import {Image} from "astro:assets";
|
||||||
{name: 'Home', href: '/'},
|
|
||||||
{
|
|
||||||
name: 'Experience↓',
|
|
||||||
href: "#",
|
|
||||||
dropdown: [
|
|
||||||
{name: 'SpaceX - Hardware Test Engineer II', href: '/experience/spacex-hardware-test-engineer'},
|
|
||||||
{
|
|
||||||
name: 'SpaceX - Avionics Test Engineer (Intern)',
|
|
||||||
href: '/experience/spacex-avionics-test-engineer-intern'
|
|
||||||
},
|
|
||||||
{name: 'SARL - Automation Engineer (Student)', href: '/experience/sarl-automation-engineer-student'},
|
|
||||||
{
|
|
||||||
name: 'CEOAS OMG - Software/Electronics Engineer (Student)',
|
|
||||||
href: '/experience/ceoas-omg-software-electronics-engineer-student'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'OSURC - Member/Officer/Sub-Team Lead (Student)',
|
|
||||||
href: '/experience/osurc-member-officer-sub-team-lead'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{name: 'Projects↓', href: '/project/project'},
|
|
||||||
{name: 'Hobbies↓', href: '/hobby/hobby'},
|
|
||||||
{name: 'Resume', href: '/resume'},
|
|
||||||
{name: 'Contact', href: '/contact'}
|
|
||||||
];
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<header class="navbar md:flex p-5 gap-5">
|
|
||||||
<Astronav>
|
<nav class="border-b-4 border-b-caperren-green text-caperren-green">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex flex-wrap items-center justify-between mx-auto p-6">
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<Image src={logoTitle} alt="Logo Title" height="50" loading="eager"/>
|
<Image src={logo_title_large}
|
||||||
</a>
|
height="56"
|
||||||
<div class="block md:hidden">
|
width="139"
|
||||||
<MenuIcon class="w-4 h-4 text-gray-800"/>
|
class="h-14 w-auto"
|
||||||
</div>
|
alt="logo title"
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<button data-collapse-toggle="navbar-multi-level" type="button"
|
||||||
|
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm md:hidden focus:ring-2 focus:ring-caperren-green"
|
||||||
|
aria-controls="navbar-multi-level" aria-expanded="false">
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M1 1h15M1 7h15M1 13h15"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="z-40 hidden w-full md:block md:w-auto" id="navbar-multi-level">
|
||||||
|
<NestedNavbarEntry items={siteLayout}/>
|
||||||
</div>
|
</div>
|
||||||
<MenuItems class="hidden md:flex">
|
</div>
|
||||||
<ul class="flex flex-col md:flex-row md:gap-5">
|
</nav>
|
||||||
<li>
|
|
||||||
<a href="/">Home</a>
|
|
||||||
</li>
|
|
||||||
<!--<li>-->
|
|
||||||
<!-- <Dropdown class="group">-->
|
|
||||||
<!-- <button class="flex items-center">-->
|
|
||||||
<!-- <span> Experience </span>-->
|
|
||||||
<!-- <svg-->
|
|
||||||
<!-- xmlns="http://www.w3.org/2000/svg"-->
|
|
||||||
<!-- fill="none"-->
|
|
||||||
<!-- viewBox="0 0 24 24"-->
|
|
||||||
<!-- stroke-width="3"-->
|
|
||||||
<!-- stroke="currentColor"-->
|
|
||||||
<!-- class="w-3 h-3 mt-0.5 group-open:rotate-180">-->
|
|
||||||
<!-- <path-->
|
|
||||||
<!-- stroke-linecap="round"-->
|
|
||||||
<!-- stroke-linejoin="round"-->
|
|
||||||
<!-- d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path>-->
|
|
||||||
<!-- </svg>-->
|
|
||||||
<!-- </button>-->
|
|
||||||
<!-- <DropdownItems class="relative">-->
|
|
||||||
<!-- <div class="bg-black px-1 py-2 absolute top-0">-->
|
|
||||||
<!-- <ul>-->
|
|
||||||
<!-- <li>-->
|
|
||||||
<!-- <a class="whitespace-nowrap" href="#">SpaceX</a>-->
|
|
||||||
<!-- </li>-->
|
|
||||||
<!-- <li>-->
|
|
||||||
<!-- <a class="whitespace-nowrap" href="#">Sinnhuber Aquatic Research Laboratory</a>-->
|
|
||||||
<!-- </li>-->
|
|
||||||
<!-- <li>-->
|
|
||||||
<!-- <a class="whitespace-nowrap" href="#">CEOAS Ocean Mixing Group</a>-->
|
|
||||||
<!-- </li>-->
|
|
||||||
<!-- </ul>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </DropdownItems>-->
|
|
||||||
<!-- </Dropdown>-->
|
|
||||||
<!--</li>-->
|
|
||||||
<!--<li>-->
|
|
||||||
<!-- <Dropdown class="group">-->
|
|
||||||
<!-- <button class="flex items-center">-->
|
|
||||||
<!-- <span> Projects </span>-->
|
|
||||||
<!-- <svg-->
|
|
||||||
<!-- xmlns="http://www.w3.org/2000/svg"-->
|
|
||||||
<!-- fill="none"-->
|
|
||||||
<!-- viewBox="0 0 24 24"-->
|
|
||||||
<!-- stroke-width="3"-->
|
|
||||||
<!-- stroke="currentColor"-->
|
|
||||||
<!-- class="w-3 h-3 mt-0.5 group-open:rotate-180">-->
|
|
||||||
<!-- <path-->
|
|
||||||
<!-- stroke-linecap="round"-->
|
|
||||||
<!-- stroke-linejoin="round"-->
|
|
||||||
<!-- d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path>-->
|
|
||||||
<!-- </svg>-->
|
|
||||||
<!-- </button>-->
|
|
||||||
<!-- <DropdownItems class="relative">-->
|
|
||||||
<!-- <div class="bg-black absolute top-0">-->
|
|
||||||
<!-- <ul>-->
|
|
||||||
<!-- <li>-->
|
|
||||||
<!-- <a href="#">Placeholder 1</a>-->
|
|
||||||
<!-- </li>-->
|
|
||||||
<!-- </ul>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </DropdownItems>-->
|
|
||||||
<!-- </Dropdown>-->
|
|
||||||
<!--</li>-->
|
|
||||||
<!--<li>-->
|
|
||||||
<!-- <Dropdown class="group">-->
|
|
||||||
<!-- <button class="flex items-center">-->
|
|
||||||
<!-- <span> Hobbies </span>-->
|
|
||||||
<!-- <svg-->
|
|
||||||
<!-- xmlns="http://www.w3.org/2000/svg"-->
|
|
||||||
<!-- fill="none"-->
|
|
||||||
<!-- viewBox="0 0 24 24"-->
|
|
||||||
<!-- stroke-width="3"-->
|
|
||||||
<!-- stroke="currentColor"-->
|
|
||||||
<!-- class="w-3 h-3 mt-0.5 group-open:rotate-180">-->
|
|
||||||
<!-- <path-->
|
|
||||||
<!-- stroke-linecap="round"-->
|
|
||||||
<!-- stroke-linejoin="round"-->
|
|
||||||
<!-- d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path>-->
|
|
||||||
<!-- </svg>-->
|
|
||||||
<!-- </button>-->
|
|
||||||
<!-- <DropdownItems class="relative">-->
|
|
||||||
<!-- <div class="bg-black absolute top-0">-->
|
|
||||||
<!-- <ul>-->
|
|
||||||
<!-- <li>Menu 1</li>-->
|
|
||||||
<!-- <li>Menu 2</li>-->
|
|
||||||
<!-- <li>Menu 3</li>-->
|
|
||||||
<!-- </ul>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </DropdownItems>-->
|
|
||||||
<!-- </Dropdown>-->
|
|
||||||
<!--</li>-->
|
|
||||||
<!--<li>-->
|
|
||||||
<!-- <a href="/resume">Resume</a>-->
|
|
||||||
<!--</li>-->
|
|
||||||
<li>
|
|
||||||
<a href="/contact">Contact</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</MenuItems>
|
|
||||||
</Astronav>
|
|
||||||
</header>
|
|
||||||
|
|||||||
47
src/components/NestedNavbarEntries.astro
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
import type {navLink} from "@interfaces/site-layout.ts";
|
||||||
|
|
||||||
|
const items: navLink[] = Astro.props.items;
|
||||||
|
const depth: number = Astro.props.depth ?? 0;
|
||||||
|
const paths: string[] = Astro.props.paths ?? [];
|
||||||
|
|
||||||
|
const getNavLinkSuffix = (entry: navLink): string => {
|
||||||
|
return "-" + [...paths, entry.path].join("-")
|
||||||
|
}
|
||||||
|
const getHrefPath = (entry: navLink): string => {
|
||||||
|
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 ? "" : "md:flex-row md:space-x-8 md:mt-0")}>
|
||||||
|
{
|
||||||
|
items.map((entry, index) => (
|
||||||
|
<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 md:hover:bg-transparent md:border-0 md:hover:text-caperren-green-light md: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"
|
||||||
|
fill="none" 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="z-10 hidden bg-black border border-caperren-green shadow-sm w-screen md:w-max">
|
||||||
|
<Astro.self items={entry.children} paths={[...paths, entry.path]} depth={depth + 1}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
) : (
|
||||||
|
|
||||||
|
<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.navText}</a>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
34
src/components/Table.astro
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
import type {tableData} from "@interfaces/table.ts";
|
||||||
|
|
||||||
|
const data: tableData = Astro.props.data;
|
||||||
|
const columnPadding: number = data.columnPadding || 2;
|
||||||
|
const rowPadding: number = data.columnPadding || 2;
|
||||||
|
const paddingClasses: string = `px-${columnPadding} py-${rowPadding}`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="relative max-w-full overflow-x-auto">
|
||||||
|
<table class="w-full text-sm text-left">
|
||||||
|
<thead class="text-xs border-b-3 border-caperren-green uppercase bg-black">
|
||||||
|
<tr>
|
||||||
|
{data.header.map(headingText => (
|
||||||
|
<th scope="col" class={paddingClasses}>
|
||||||
|
{headingText}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.rows.map(row => (
|
||||||
|
<tr class=" border-b dark:bg-black border-caperren-green">
|
||||||
|
{row.map(rowColumnText => (
|
||||||
|
<th scope="row"
|
||||||
|
class={paddingClasses + " font-medium whitespace-nowrap"}>
|
||||||
|
{rowColumnText}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
34
src/components/Timeline/Timeline.astro
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
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 src="./timeline.ts"/>
|
||||||
44
src/components/Timeline/timeline.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import LeaderLine from "leader-line-new";
|
||||||
|
|
||||||
|
class CustomTimeline extends HTMLElement {
|
||||||
|
_eventElements: Element[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._eventElements = this._getNodeElements();
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener("load", this._paintLeaderLines);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_getNodeElements = (): Element[] =>
|
||||||
|
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)
|
||||||
174
src/data/site-layout.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import type {navLink} from "@interfaces/site-layout.ts"
|
||||||
|
|
||||||
|
export const siteLayout: navLink[] = [
|
||||||
|
{navText: "About", path: ""},
|
||||||
|
{navText: "Education", path: "education"},
|
||||||
|
{
|
||||||
|
navText: "Experiences",
|
||||||
|
path: "experience",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
navText: "SpaceX",
|
||||||
|
path: "spacex",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
navText: "Hardware Test Engineer I/II",
|
||||||
|
path: "hardware-test-engineer-i-ii"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "Avionics Test Engineering Internship",
|
||||||
|
path: "avionics-test-engineering-internship"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "OSU CEOAS",
|
||||||
|
path: "osu-ceoas-ocean-mixing-group",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
navText: "Robotics Oceanographic Surface Sampler",
|
||||||
|
path: "robotic-oceanographic-surface-sampler",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "LeConte Glacier Deployments",
|
||||||
|
path: "leconte-glacier-deployments",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "OSU SARL",
|
||||||
|
path: "osu-sinnhuber-aquatic-research-laboratory",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
navText: "Team Lead",
|
||||||
|
path: "team-lead",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "Zebrafish Embryo Pick and Plate",
|
||||||
|
path: "zebrafish-embryo-pick-and-plate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "Shuttlebox Behavior System",
|
||||||
|
path: "shuttlebox-behavior-system",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "Dechorionator",
|
||||||
|
path: "dechorionator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "Denso Embryo Pick and Plate",
|
||||||
|
path: "denso-embryo-pick-and-plate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "ZScan Processor",
|
||||||
|
path: "zscan-processor",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "OSU Robotics Club",
|
||||||
|
path: "osu-robotics-club",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
navText: "Mars Rover Software Team Lead",
|
||||||
|
path: "mars-rover-software-team-lead",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "Mars Rover Emergency Software Team Lead",
|
||||||
|
path: "mars-rover-emergency-software-team-lead",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "Mars Rover Electrical Team Lead",
|
||||||
|
path: "mars-rover-electrical-team-lead",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "Club Officer",
|
||||||
|
path: "club-officer",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "Hobbies",
|
||||||
|
path: "hobby",
|
||||||
|
children: [
|
||||||
|
|
||||||
|
{
|
||||||
|
navText: "Homelab", path: "homelab",
|
||||||
|
children: [
|
||||||
|
{navText: "Home Server Rack", path: "home-server-rack"},
|
||||||
|
{navText: "Offsite Backup Rack", path: "offsite-backup-rack"},
|
||||||
|
{navText: "Kubernetes Cluster", path: "kubernetes-cluster"},
|
||||||
|
{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"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: "Trips",
|
||||||
|
// path: "trips",
|
||||||
|
// children: [
|
||||||
|
// {title: "2025-08 | Alaska ", path: "2025-08-alaska"},
|
||||||
|
// {title: "2024-10 | Norway ", path: "2024-10-norway"}
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: "Projects", path: "projects",
|
||||||
|
// children: [
|
||||||
|
// {title: "Shed Solar", path: "shed-solar"},
|
||||||
|
// {title: "OSSM Overkill Edition", path: "ossm-overkill-edition"},
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
{navText: "NixOS", path: "nixos"},
|
||||||
|
{navText: "Body Mods", path: "body-mods"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navText: "Resumes",
|
||||||
|
path: "resume",
|
||||||
|
children: [
|
||||||
|
// {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"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{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;
|
||||||
|
}
|
||||||
1
src/env.d.ts
vendored
@@ -1,6 +1,5 @@
|
|||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly PUBLIC_REPO_VERSION_HASH: string;
|
readonly PUBLIC_REPO_VERSION_HASH: string;
|
||||||
readonly PUBLIC_PROJECT_VERSION: string;
|
|
||||||
readonly PUBLIC_BUILD_ENVIRONMENT: string;
|
readonly PUBLIC_BUILD_ENVIRONMENT: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
src/interfaces/experience-metadata.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
export interface experience {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
export interface subExperience {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
startDate: DateTime;
|
||||||
|
endDate?: DateTime;
|
||||||
|
}
|
||||||
5
src/interfaces/image-carousel.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface carouselGroup {
|
||||||
|
animation: "static" | "slide";
|
||||||
|
interval?: number;
|
||||||
|
images: ImageMetadata[];
|
||||||
|
}
|
||||||
6
src/interfaces/site-layout.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface navLink {
|
||||||
|
navText: string;
|
||||||
|
path?: string;
|
||||||
|
pubpath?: string;
|
||||||
|
children?: navLink[];
|
||||||
|
}
|
||||||
6
src/interfaces/table.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface tableData {
|
||||||
|
header: string[];
|
||||||
|
columnPadding?: number;
|
||||||
|
rowPadding?: number;
|
||||||
|
rows: Array<Array<any>>;
|
||||||
|
}
|
||||||
6
src/interfaces/timeline.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface timelineEntry {
|
||||||
|
event: string;
|
||||||
|
eventDetail?: string
|
||||||
|
date: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
import '../styles/global.css'
|
import '@styles/global.css'
|
||||||
import Navbar from '../components/Navbar.astro';
|
|
||||||
import Footer from '../components/Footer.astro';
|
import Navbar from '@components/Navbar.astro';
|
||||||
|
import Footer from '@components/Footer.astro';
|
||||||
|
|
||||||
const pageTitle = Astro.props.title ? `${Astro.props.title} - Corwin Perren` : "Corwin Perren";
|
const pageTitle = Astro.props.title ? `${Astro.props.title} - Corwin Perren` : "Corwin Perren";
|
||||||
---
|
---
|
||||||
@@ -9,15 +10,20 @@ const pageTitle = Astro.props.title ? `${Astro.props.title} - Corwin Perren` : "
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
<link rel="icon" href="/favicon-solid.png" type="image/png"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>{pageTitle}</title>
|
<title>{pageTitle}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="bg-black text-white">
|
||||||
<Navbar/>
|
<Navbar/>
|
||||||
<main style="padding: 2rem;">
|
<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/>
|
<slot/>
|
||||||
</main>
|
</main>
|
||||||
<Footer/>
|
<Footer/>
|
||||||
|
<script src="../scripts/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from './BaseLayout.astro';
|
import BaseLayout from './BaseLayout.astro';
|
||||||
---
|
---
|
||||||
<BaseLayout>
|
<BaseLayout title={Astro.props.title}>
|
||||||
<slot/>
|
<slot/>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
6
src/layouts/HobbyLayout.astro
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from './BaseLayout.astro';
|
||||||
|
---
|
||||||
|
<BaseLayout title={Astro.props.title}>
|
||||||
|
<slot/>
|
||||||
|
</BaseLayout>
|
||||||
10
src/layouts/ResumeLayout.astro
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from './BaseLayout.astro';
|
||||||
|
|
||||||
|
const resume = Astro.props.resume;
|
||||||
|
---
|
||||||
|
<BaseLayout>
|
||||||
|
<div class="h-dvh">
|
||||||
|
<iframe src={resume} class="mx-auto w-9/10 md:w-3/5 h-7/10 md:h-4/5"/>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout>
|
|
||||||
<div>Email:
|
|
||||||
<a href="mailto:caperren@caperren.com">caperren@caperren.com</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Phone:
|
|
||||||
<a href="tel:360-298-4396">360-298-4396</a>
|
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
|
||||||
117
src/pages/education.astro
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from "@layouts/BaseLayout.astro";
|
||||||
|
import Carousel from "@components/CustomCarousel/CustomCarousel.astro";
|
||||||
|
import Timeline from "@components/Timeline/Timeline.astro";
|
||||||
|
import Table from "@components/Table.astro";
|
||||||
|
|
||||||
|
import type {carouselGroup} from "@interfaces/image-carousel.ts";
|
||||||
|
import type {tableData} from "@interfaces/table.ts";
|
||||||
|
import type {timelineEntry} from "@interfaces/timeline.ts";
|
||||||
|
|
||||||
|
import fhhs_diploma from "@assets/education/fhhs-diploma.jpg";
|
||||||
|
import osu_bs_cs_diploma from "@assets/education/osu-bs-cs-diploma.jpg"
|
||||||
|
|
||||||
|
|
||||||
|
const diplomaCarouselGroup: carouselGroup = {
|
||||||
|
animation: "slide",
|
||||||
|
images: [
|
||||||
|
fhhs_diploma,
|
||||||
|
osu_bs_cs_diploma
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeline: timelineEntry[] = [
|
||||||
|
{
|
||||||
|
event: "High School Diploma",
|
||||||
|
eventDetail: "Friday Harbor High School",
|
||||||
|
date: "June 2011"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: "B.S. Computer Science",
|
||||||
|
eventDetail: "Oregon State University",
|
||||||
|
date: "June 2019"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const courseTable: tableData = {
|
||||||
|
header: ["Program", "Course", "Description"],
|
||||||
|
rows: [
|
||||||
|
["CS", "LDT", "INTRO TO LINUX"],
|
||||||
|
["CS", "261", "DATA STRUCTURES"],
|
||||||
|
["CS", "271", "COMPUTER ARCH & ASSEM LANGUAGE"],
|
||||||
|
["CS", "290", "WEB DEVELOPMENT"],
|
||||||
|
["CS", "312", "SYSTEM ADMINISTRATION"],
|
||||||
|
["CS", "325", "ANALYSIS OF ALGORITHMS"],
|
||||||
|
["CS", "331", "INTRO ARTIFICIAL INTELLIGENCE"],
|
||||||
|
["CS", "340", "INTRODUCTION TO DATABASES"],
|
||||||
|
["CS", "344", "OPERATING SYSTEMS I"],
|
||||||
|
["CS", "352", "INTRO TO USABILITY ENGINEERING"],
|
||||||
|
["CS", "361", "SOFTWARE ENGINEERING I"],
|
||||||
|
["CS", "362", "SOFTWARE ENGINEERING II"],
|
||||||
|
["CS", "370", "INTRODUCTION TO SECURITY"],
|
||||||
|
["CS", "372", "INTRO TO COMPUTER NETWORKS"],
|
||||||
|
["CS", "381", "PROGRAMMING LANGUAGE FUND"],
|
||||||
|
["CS", "391", "SOC & ETHICAL ISSUES IN COMSC"],
|
||||||
|
["CS", "444", "OPERATING SYSTEMS II"],
|
||||||
|
["CS", "461", "SENIOR SOFTWARE ENGR PROJECT I"],
|
||||||
|
["CS", "462", "SENIOR SOFTWARE ENGR PROJECT II"],
|
||||||
|
["CS", "463", "SENIOR SOFTWARE ENGR PROJECT III"],
|
||||||
|
["CS", "464", "OPEN SOURCE SOFTWARE"],
|
||||||
|
["CS", "468", "INCLUSIVE DESIGN (HCI)"],
|
||||||
|
["CS", "496", "MOBILE/CLOUD SOFTWARE DEVEL"],
|
||||||
|
["ECE", "111", "INTRODUCTION TO ECE: TOOLS"],
|
||||||
|
["ECE", "112", "INTRODUCTION TO ECE: CONCEPTS"],
|
||||||
|
["ECE", "151", "PROGRAMMING I/EMBED CONTR LAB"],
|
||||||
|
["ECE", "152", "PROGRAMMING II/EMBED CONTR LAB"],
|
||||||
|
["ECE", "271", "DIGITAL LOGIC DESIGN"],
|
||||||
|
["ECE", "272", "DIGITAL LOGIC DESIGN LAB"],
|
||||||
|
["ECE", "375", "COMPUTER ORG & ASSEMBLY LANG"],
|
||||||
|
["ENGR", "201", "ELECTRICAL FUNDAMENTALS I"],
|
||||||
|
["ENGR", "202", "ELECTRICAL FUNDAMENTALS II"],
|
||||||
|
["ENGR", "391", "ENGINEERING ECON & PROJ MGMT"],
|
||||||
|
["ROB", "421", "APPLIED ROBOTICS"],
|
||||||
|
["ROB", "456", "INTELLIGENT ROBOTS"],
|
||||||
|
["MTH", "231", "ELEMENTS DISCRETE MATH"],
|
||||||
|
["MTH", "241", "CALC FOR MGT & SOCIAL SCI"],
|
||||||
|
["MTH", "251", "DIFFERENTIAL CALCULUS"],
|
||||||
|
["MTH", "252", "INTEGRAL CALCULUS"],
|
||||||
|
["MTH", "254", "VECTOR CALCULUS I"],
|
||||||
|
["MTH", "306", "MATRIX & POWER SERIES METHODS"],
|
||||||
|
["ST", "314", "INTRO TO STATS FOR ENGINEERS"],
|
||||||
|
["PH", "211", "GENERAL PHYSICS WITH CALCULUS I"],
|
||||||
|
["PH", "212", "GENERAL PHYSICS WITH CALCULUS II"],
|
||||||
|
["PH", "213", "GENERAL PHYSICS WITH CALCULUS III"],
|
||||||
|
["WR", "LDT", "COMPOSITION III"],
|
||||||
|
["WR", "LDT", "CREATIVE WRITING"],
|
||||||
|
["WR", "121", "ENGLISH COMPOSITION"],
|
||||||
|
["WR", "214", "WRITING IN BUSINESS"],
|
||||||
|
["WR", "327", "TECHNICAL WRITING"],
|
||||||
|
["BI", "102", "GENERAL BIOLOGY"],
|
||||||
|
["BI", "349", "BIODIVERSITY: CAUSES, CONSERV"],
|
||||||
|
["CH", "201", "CHEMISTRY FOR ENGINEERING MAJ"],
|
||||||
|
["CH", "211", "RECITATION FOR CHEMISTRY 201"],
|
||||||
|
["COMM", "114", "ARGUMENT & CRITICAL DISCOURSE"],
|
||||||
|
["GEO", "LDT", "SURVEY EARTH SCIENCE"],
|
||||||
|
["HDFS", "240", "HUMAN SEXUALITY"],
|
||||||
|
["HHS", "231", "LIFETIME FITNESS FOR HEALTH"],
|
||||||
|
["HHS", "246", "LIFETIME FITNESS: WALKING"],
|
||||||
|
["MUS", "LDT", "LA: MUSIC APPRECIATION"],
|
||||||
|
["MUS", "108", "MUSIC CULTURES/ NAT AM FLUTE"],
|
||||||
|
["PAC", "123", "BOWLING I"],
|
||||||
|
["PHL", "205", "ETHICS"],
|
||||||
|
["PS", "LDT", "AMERICAN GOVT"],
|
||||||
|
["PSY", "201", "GENERAL PSYCHOLOGY"],
|
||||||
|
["QS", "262", "INTRODUCTION TO QUEER STUDIES"]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
---
|
||||||
|
<BaseLayout title="Education">
|
||||||
|
<Carousel carouselGroup={diplomaCarouselGroup}/>
|
||||||
|
<h2 class="font-bold md:text-2xl my-4 underline">Timeline</h2>
|
||||||
|
<Timeline timeline={timeline}/>
|
||||||
|
<h2 class="font-bold md:text-2xl my-4 underline">Oregon State University</h2>
|
||||||
|
<a class="font-bold md:text-lg my-4 text-blue-500 underline hover:text-blue-300"
|
||||||
|
href="https://github.com/caperren/school_archives/tree/master/OSU%20Coursework">Coursework Archives</a>
|
||||||
|
<h3 class="font-bold md:text-lg my-4 underline">Course Listing</h3>
|
||||||
|
<Table data={courseTable}/>
|
||||||
|
</BaseLayout>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ExperienceLayout title="CEOAS - LeConte Glacier Deployments">
|
||||||
|
</ExperienceLayout>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ExperienceLayout title="CEOAS - Robotic Oceanographic Surface Sampler">
|
||||||
|
</ExperienceLayout>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ExperienceLayout title="OSURC - Officer">
|
||||||
|
</ExperienceLayout>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ExperienceLayout title="OSURC - Electrical Team Lead">
|
||||||
|
</ExperienceLayout>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ExperienceLayout title="OSURC - Emergency Software Team Lead">
|
||||||
|
</ExperienceLayout>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ExperienceLayout title="OSURC - Software Team Lead">
|
||||||
|
</ExperienceLayout>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ExperienceLayout title="SARL - Dechorionator">
|
||||||
|
</ExperienceLayout>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ExperienceLayout title="SARL - Denso Embryo Pick and Plate">
|
||||||
|
</ExperienceLayout>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ExperienceLayout title="SARL - Shuttlebox Behavior System">
|
||||||
|
</ExperienceLayout>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ExperienceLayout title="SARL - Team Lead">
|
||||||
|
</ExperienceLayout>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ExperienceLayout title="SARL - Zebrafish Embryo Pick and Plate">
|
||||||
|
</ExperienceLayout>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ExperienceLayout title="SARL - ZScan Processor">
|
||||||
|
</ExperienceLayout>
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
import ExperienceLayout from '../../../layouts/ExperienceLayout.astro';
|
|
||||||
import {Image} from 'astro:assets';
|
|
||||||
|
|
||||||
import spring_2019_interns from "../../../assets/experience/spacex/avionics-test-engineering-intern/spring-2019-interns.jpg";
|
|
||||||
---
|
|
||||||
<ExperienceLayout>
|
|
||||||
<Image class="mx-auto block" src={spring_2019_interns} alt="spring-2019-interns.jpg" loading="eager"/>
|
|
||||||
</ExperienceLayout>
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
import ExperienceLayout from '@layouts/ExperienceLayout.astro';
|
||||||
|
import Timeline from '@components/Timeline/Timeline.astro';
|
||||||
|
import Carousel from "@components/CustomCarousel/CustomCarousel.astro";
|
||||||
|
|
||||||
|
import spring_2019_interns from "@assets/experience/spacex/avionics-test-engineering-internship/spring-2019-interns.jpg";
|
||||||
|
|
||||||
|
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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Though I did get to work on some really fun projects during my internship at SpaceX, I unfortunately can’t go into much detail due to NDA’s and ITAR 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 I’ve done really helped me out here, as the framework SpaceX has created was quite complex and would otherwise have been fairly difficult to write code for. I also wrote a simple tool for automating the creation of Jira work tickets so that the two teams that ended up using it wouldn’t have to have their members manually creating dozens of them as work and issues came in through a separate system.
|
||||||
|
|
||||||
|
I was also quite happy in that I got to perform some circuit debugging on avionics test system hardware, both for my project and for a separate test system. A final experience I had here was getting to work directly with the head engineer from a company that supplied a piece of test hardware I was interfacing with. It was quite incredible to see just how much weight a SpaceX email address had when trying to solve problems I had 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 from clubs and small labs where a support email might not even get a response for months, it was quite a refreshing experience.
|
||||||
|
|
||||||
|
</ExperienceLayout>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
import ExperienceLayout from '@layouts/ExperienceLayout.astro';
|
||||||
|
import Timeline from '@components/Timeline/Timeline.astro';
|
||||||
|
import Carousel from "@components/CustomCarousel/CustomCarousel.astro";
|
||||||
|
|
||||||
|
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 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>
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
import ExperienceLayout from '../../../layouts/ExperienceLayout.astro';
|
|
||||||
import {Image} from 'astro:assets';
|
|
||||||
|
|
||||||
import starlink_headquarters_selfie from "../../../assets/experience/spacex/hardware-test-engineer-ii/starlink_headquarters_selfie.jpg";
|
|
||||||
---
|
|
||||||
<ExperienceLayout>
|
|
||||||
<Image class="mx-auto block" src={starlink_headquarters_selfie} alt="starlink_headquarters_selfie" loading="eager"/>
|
|
||||||
</ExperienceLayout>
|
|
||||||
28
src/pages/hobby/body-mods.astro
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
import HobbyLayout from "@layouts/HobbyLayout.astro";
|
||||||
|
import Carousel from "@components/CustomCarousel/CustomCarousel.astro";
|
||||||
|
|
||||||
|
import type {carouselGroup} from "@interfaces/image-carousel.ts";
|
||||||
|
|
||||||
|
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 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";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const rfidImplantCarouselGroup: carouselGroup = {
|
||||||
|
animation: "slide",
|
||||||
|
images: [
|
||||||
|
xem_pouch,
|
||||||
|
injector_exploded,
|
||||||
|
quarter_euro_transponder,
|
||||||
|
injection_site
|
||||||
|
]
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<HobbyLayout title="Body Mods">
|
||||||
|
<h2 class="font-bold md:text-2xl my-4 underline">RFID Implant</h2>
|
||||||
|
<Carousel carouselGroup={rfidImplantCarouselGroup}/>
|
||||||
|
</HobbyLayout>
|
||||||