Compare commits

...

21 Commits

Author SHA1 Message Date
817c6076dc Merge pull request 'More website content updates' (#10) from website-content-updates into main
Some checks failed
Build and Test - Production / test (push) Failing after 3m2s
Build and Test - Production / build_and_push (push) Has been skipped
Build and Test - Production / deploy_production (push) Has been skipped
Reviewed-on: #10
2025-12-05 10:00:13 +00:00
4847e9f172 Linting
All checks were successful
Build and Test - Staging / test (pull_request) Successful in 6m4s
Build and Test - Staging / build_and_push (pull_request) Successful in 6m28s
Build and Test - Staging / deploy_staging (pull_request) Successful in 3s
2025-12-05 01:37:46 -08:00
3aa75e1a10 LeConte deployments page complete, better auto-formatting and import sorting, new inline link, popover definitions, and paragraph components, improvements to component interfaces
Some checks failed
Build and Test - Staging / test (pull_request) Failing after 2m31s
Build and Test - Staging / build_and_push (pull_request) Has been skipped
Build and Test - Staging / deploy_staging (pull_request) Has been skipped
2025-12-05 01:02:18 -08:00
91cd9af0f8 Added pictures for CEOAS, formatting fixes, new work to dictionary
All checks were successful
Build and Test - Staging / test (pull_request) Successful in 4m6s
Build and Test - Staging / build_and_push (pull_request) Successful in 4m6s
Build and Test - Staging / deploy_staging (pull_request) Successful in 3s
2025-11-30 19:44:24 -08:00
4a59e44716 Started refactoring, added prettier and checks and reformatted project, added cspell and checks and custom project words, beginning of robotic oceanographic surface sampler content
Some checks failed
Build and Test - Staging / test (pull_request) Failing after 2m35s
Build and Test - Staging / build_and_push (pull_request) Has been skipped
Build and Test - Staging / deploy_staging (pull_request) Has been skipped
2025-11-30 15:48:36 -08:00
67eb549ed2 Removed logging, updated packages
All checks were successful
Build and Test - Staging / test (pull_request) Successful in 3m18s
Build and Test - Staging / build_and_push (pull_request) Successful in 3m12s
Build and Test - Staging / deploy_staging (pull_request) Successful in 3s
2025-11-30 10:58:31 -08:00
8fd009ffda Remove blank line
All checks were successful
Build and Test - Staging / test (pull_request) Successful in 3m48s
Build and Test - Staging / build_and_push (pull_request) Successful in 3m31s
Build and Test - Staging / deploy_staging (pull_request) Successful in 4s
2025-11-12 01:48:42 -08:00
4b1eb3777f Removed Timeline fixed height that was only used for testing 2025-11-12 01:36:35 -08:00
7858d95f58 Reformatted the project 2025-11-12 01:21:48 -08:00
b91d37db45 Fixed navbar button border and list overlap 2025-11-12 01:14:54 -08:00
109996989e No major minor versions on actions
All checks were successful
Build and Test - Staging / test (pull_request) Successful in 11m13s
Build and Test - Staging / build_and_push (pull_request) Successful in 4m18s
Build and Test - Staging / deploy_staging (pull_request) Successful in 5s
2025-11-12 00:36:01 -08:00
96151d6512 Custom set difference for tests
Some checks failed
Build and Test - Staging / build_and_push (pull_request) Failing after 2s
Build and Test - Staging / test (pull_request) Successful in 16m27s
Build and Test - Staging / deploy_staging (pull_request) Successful in 3s
2025-11-12 00:17:15 -08:00
ac266b98ec Specify node version
Some checks failed
Build and Test - Staging / test (pull_request) Successful in 4m37s
Build and Test - Staging / build_and_push (pull_request) Failing after 37s
Build and Test - Staging / deploy_staging (pull_request) Successful in 7s
2025-11-11 23:40:14 -08:00
8fa3c0b3ab Update docker and node action versions
Some checks failed
Build and Test - Staging / test (pull_request) Failing after 9m12s
Build and Test - Staging / build_and_push (pull_request) Has been skipped
Build and Test - Staging / deploy_staging (pull_request) Has been skipped
2025-11-11 23:29:21 -08:00
cef1b3381f Make endpoint tests not async and more logging
Some checks failed
Build and Test - Staging / test (pull_request) Failing after 3m29s
Build and Test - Staging / build_and_push (pull_request) Has been skipped
Build and Test - Staging / deploy_staging (pull_request) Has been skipped
2025-11-11 23:12:45 -08:00
049861c255 Changes to endpoint unit tests for debugging
Some checks failed
Build and Test - Staging / test (pull_request) Failing after 3m36s
Build and Test - Staging / build_and_push (pull_request) Has been skipped
Build and Test - Staging / deploy_staging (pull_request) Has been skipped
2025-11-11 23:00:11 -08:00
ba948e4181 Fixed Timeline svg draws making window too wide, adjusted navbar to perform responsive transition at a larger screen size, removed more obsolete attrs
Some checks failed
Build and Test - Staging / test (pull_request) Failing after 3m29s
Build and Test - Staging / build_and_push (pull_request) Has been skipped
Build and Test - Staging / deploy_staging (pull_request) Has been skipped
2025-11-11 22:53:05 -08:00
68b6d7f785 Fixed favicon mis-centering, and higher resolution versions, slowed carousel animation for less choppy results, removed obsolete attrs
Some checks failed
Build and Test - Staging / test (pull_request) Failing after 7m43s
Build and Test - Staging / build_and_push (pull_request) Has been skipped
Build and Test - Staging / deploy_staging (pull_request) Has been skipped
2025-11-11 17:57:37 -08:00
1f9264a409 Fixed favicon mis-centering, and higher resolution versions
Some checks failed
Build and Test - Staging / test (pull_request) Failing after 3m47s
Build and Test - Staging / build_and_push (pull_request) Has been skipped
Build and Test - Staging / deploy_staging (pull_request) Has been skipped
2025-11-11 17:39:39 -08:00
7774e31c36 Better resolution headshot, fullscreen modal for carousel, no fixed height and width on logo title for better resolution, spacing between dropdown entries in navbar 2025-11-11 17:19:38 -08:00
c9f921ba5b Fixed weird floating footer behavior, updated Timeline to handle new growing scrolling content div, fixed grammar issue on chubby buttons, removed used nginx configs and Dockerfile comments, made Table not go full-w automatically, added tests to ensure no orphaned astro pages or site layout entries exist, dummy sitemap index so code analysis doesn't freak out
Some checks failed
Build and Test - Staging / test (pull_request) Failing after 3m28s
Build and Test - Staging / build_and_push (pull_request) Has been skipped
Build and Test - Staging / deploy_staging (pull_request) Has been skipped
2025-11-11 15:16:15 -08:00
129 changed files with 4383 additions and 2053 deletions

View File

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

7
.editorconfig Normal file
View File

@@ -0,0 +1,7 @@
[*]
charset = utf-8
insert_final_newline = true
end_of_line = lf
indent_style = space
indent_size = 2
max_line_length = 80

View File

@@ -1,27 +1,27 @@
name: Playwright Tests name: Playwright Tests
on: on:
push: push:
branches: [ main, master ] branches: [main, master]
pull_request: pull_request:
branches: [ main, master ] branches: [main, master]
jobs: jobs:
test: test:
timeout-minutes: 60 timeout-minutes: 60
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: lts/* node-version: lts/*
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: npx playwright install --with-deps run: npx playwright install --with-deps
- name: Run Playwright tests - name: Run Playwright tests
run: npx playwright test run: npx playwright test
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
with: with:
name: playwright-report name: playwright-report
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30

View File

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

View File

@@ -1,7 +1,7 @@
name: Build and Test - Staging name: Build and Test - Staging
on: on:
pull_request: pull_request:
types: [ opened, synchronize, reopened ] types: [opened, synchronize, reopened]
jobs: jobs:
test: test:
@@ -12,16 +12,24 @@ jobs:
project_version: ${{ steps.project_metadata.outputs.PROJECT_VERSION }} project_version: ${{ steps.project_metadata.outputs.PROJECT_VERSION }}
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Node Environment - name: Setup Node Environment
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with:
node-version: ">=22.20"
- name: Setup Project Dependencies - name: Setup Project Dependencies
run: | run: |
npm ci npm ci
npx playwright install --with-deps npx playwright install --with-deps
- name: Code Formatting Check
run: npx prettier . --check
- name: Spelling Check
run: npx cspell .
- name: Build Project - name: Build Project
run: npm run build run: npm run build
@@ -42,7 +50,7 @@ jobs:
needs: test needs: test
steps: steps:
- name: Checkout caperren-com Repository - name: Checkout caperren-com Repository
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
path: caperren-com path: caperren-com
@@ -57,7 +65,7 @@ jobs:
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Build and Push - name: Build and Push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: caperren-com context: caperren-com
push: true push: true
@@ -98,4 +106,4 @@ jobs:
-H 'accept: */*' \ -H 'accept: */*' \
-H 'Authorization: Bearer ${{ secrets.TRUENAS_CAPERRENCOM_API_KEY }}' \ -H 'Authorization: Bearer ${{ secrets.TRUENAS_CAPERRENCOM_API_KEY }}' \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '"caperren-com-stg"' -d '"caperren-com-stg"'

9
.gitignore vendored
View File

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

34
.prettierignore Normal file
View File

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

29
.prettierrc Normal file
View File

@@ -0,0 +1,29 @@
{
"astroOrganizeImportsMode": "All",
"importOrder": [
"^@core/(.*)$",
"^@server/(.*)$",
"^@ui/(.*)$",
"^@layouts/(.*)$",
"^@components/(.*)$",
"^@interfaces/(.*)$",
"^@assets/(.*)$",
"^[./]"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": [
"prettier-plugin-astro",
"prettier-plugin-tailwindcss",
"@trivago/prettier-plugin-sort-imports",
"prettier-plugin-astro-organize-imports"
],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
]
}

View File

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

View File

@@ -6,7 +6,16 @@
astro_upgrade \ astro_upgrade \
build \ build \
dev \ dev \
dev-hosted dev-hosted \
test \
_spelling-generate-new-words \
spelling-find-new-words \
spelling-add-new-words \
spelling-check \
cleanup-check \
cleanup-code \
convert_video \
convert_video_times
default: dev default: dev
@@ -28,3 +37,55 @@ dev:
dev-hosted: dev-hosted:
npm run dev-hosted npm run dev-hosted
test: spelling-check
@npx playwright install --with-deps
npm run test --ui
npx playwright test --ui
_spelling-generate-new-words:
@cspell --words-only --unique . 2>/dev/null | sort --ignore-case -o new-words.txt
spelling-find-new-words: _spelling-generate-new-words
@echo "Found the following new words:"
@cat new-words.txt
@rm -f new-words.txt
spelling-add-new-words: _spelling-generate-new-words
@echo "Adding to project-words.txt"
@cat new-words.txt >> project-words.txt
@rm -f new-words.txt
@cat project-words.txt | sort --ignore-case -o project-words.txt
spelling-check:
npx cspell .
cleanup-check:
npx prettier . --check
cleanup-code:
npx prettier . --write
convert_video:
ffmpeg \
-init_hw_device vaapi=va:/dev/dri/renderD128 \
-filter_hw_device va \
-i $(input) \
-vf 'format=nv12,hwupload,scale_vaapi=-2:720' \
-c:v h264_vaapi \
-rc_mode CQP \
-qp 28 \
-an \
$(extra_args) \
$(output)
convert_video_times:
ffmpeg \
-init_hw_device vaapi=va:/dev/dri/renderD128 \
-filter_hw_device va \
-i $(input) \
-vf 'format=nv12,hwupload,scale_vaapi=-2:720,trim=start=$(start):end=$(end)' \
-c:v h264_vaapi \
-rc_mode CQP \
-qp 28 \
-an \
$(output)

View File

@@ -1,48 +1,3 @@
# Astro Starter Kit: Basics # Corwin Perren's Personal Portfolio Website
```sh Check the Makefile and/or package.json for the commands needed to build and run this project.
npm create astro@latest -- --template basics
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
│ └── favicon.svg
├── src/
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ └── index.astro
└── package.json
```
To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

View File

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

23
cspell.json Normal file
View File

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

View File

@@ -1,14 +0,0 @@
#!/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;'

View File

@@ -14,12 +14,6 @@ http {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html index.htm; index index.html index.htm;
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
#include /etc/nginx/conf.d/_release_token.conf;
#etag off;
#add_header ETag "\"W/$release_token\"" always;
#add_header Cache-Control "max-age=0, must-revalidate" always;
gzip on; gzip on;
gzip_proxied any; gzip_proxied any;

2078
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
{ {
"name": "caperren-com", "name": "caperren-com",
"type": "module", "type": "module",
"version": "0.0.1",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"dev-hosted": "astro dev --host", "dev-hosted": "astro dev --host",
@@ -14,7 +13,7 @@
"dependencies": { "dependencies": {
"@astrojs/sitemap": "^3.6.0", "@astrojs/sitemap": "^3.6.0",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"astro": "^5.15.4", "astro": "^5.16.3",
"flowbite": "^3.1.2", "flowbite": "^3.1.2",
"leader-line-new": "^1.1.9", "leader-line-new": "^1.1.9",
"luxon": "^3.7.2", "luxon": "^3.7.2",
@@ -23,8 +22,14 @@
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.56.1", "@playwright/test": "^1.56.1",
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
"@types/luxon": "^3.7.1", "@types/luxon": "^3.7.1",
"@types/node": "^24.10.0", "@types/node": "^24.10.0",
"cspell": "^9.3.2",
"prettier": "3.7.3",
"prettier-plugin-astro": "0.14.1",
"prettier-plugin-astro-organize-imports": "^0.4.11",
"prettier-plugin-tailwindcss": "0.7.1",
"vitest": "^4.0.7" "vitest": "^4.0.7"
} }
} }

View File

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

51
project-words.txt Normal file
View File

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

BIN
public/16x16-favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

BIN
public/180x180-favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
public/192x192-favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
public/32x32-favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

BIN
public/48x48-favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

BIN
public/512x512-favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 956 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@@ -1,151 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
class="logo-title"
width="21.129419mm"
height="9.3295746mm"
viewBox="0 0 74.868021 33.057548"
id="svg4376"
version="1.1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="logo-title.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4378" />
<sodipodi:namedview
id="base"
pagecolor="#000000"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="11.313709"
inkscape:cx="329.77692"
inkscape:cy="530.2417"
inkscape:document-units="px"
inkscape:current-layer="layer2"
showgrid="false"
inkscape:window-width="2546"
inkscape:window-height="1386"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:export-bgcolor="#ffffff00">
<inkscape:page
x="0"
y="0"
width="74.868019"
height="33.057549"
id="page2"
margin="0"
bleed="0" />
</sodipodi:namedview>
<metadata
id="metadata4381">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-262.90298,-476.37904)">
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#10ac25;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="279.60336"
y="517.86682"
id="text4159-4"
transform="scale(1.0283363,0.97244452)"
inkscape:export-xdpi="120.07"
inkscape:export-ydpi="120.07"
inkscape:export-filename="text4159-4.svg"><tspan
sodipodi:role="line"
x="279.60336"
y="517.86682"
style="font-size:15.425px;line-height:1.25"
id="tspan4167-2">Perren</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
transform="translate(-262.90298,-476.37904)">
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#10ac25;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="260.65689"
y="506.06656"
id="text3344-4-5"
inkscape:export-xdpi="120.07"
inkscape:export-ydpi="120.07"><tspan
sodipodi:role="line"
id="tspan3346-7-6"
x="260.65689"
y="506.06656"
style="font-size:40px;line-height:1.25">C</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#10ac25;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="270.34253"
y="498.55893"
id="text3348-1-8"
transform="scale(1.0028223,0.99718564)"
inkscape:export-xdpi="120.07"
inkscape:export-ydpi="120.07"><tspan
sodipodi:role="line"
id="tspan3350-3-1"
x="270.34253"
y="498.55893"
style="font-size:16.283px;line-height:1.25">A</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;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:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="96.614212"
y="537.43079"
id="text3344-5-7-6"
transform="matrix(0.88187751,-0.35574603,0.36069903,0.98843971,0,0)"
inkscape:export-xdpi="120.07"
inkscape:export-ydpi="120.07"><tspan
sodipodi:role="line"
id="tspan3370-7-8"
x="96.614212"
y="537.43079"
style="font-size:14.587px;line-height:1.25">P</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#10ac25;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="287.28516"
y="490.68661"
id="text4159"
inkscape:export-xdpi="120.07"
inkscape:export-ydpi="120.07"><tspan
sodipodi:role="line"
id="tspan4161"
x="287.28516"
y="490.68661"
style="font-size:15px;line-height:1.25">Corwin</tspan><tspan
sodipodi:role="line"
x="287.28516"
y="509.43661"
style="font-size:15px;line-height:1.25"
id="tspan4167"> </tspan></text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -1,96 +0,0 @@
---
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"/>

View File

@@ -1,80 +0,0 @@
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)

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
---
interface Props {
href: string;
target?: string;
}
const { href, target = "_blank" } = Astro.props;
---
<>
<a class="text-blue-500 hover:text-blue-300" href={href} target={target}>
<slot />
</a>
</>

View File

@@ -0,0 +1,18 @@
---
interface Props {
title: string;
href: string;
target?: string;
}
const { title, href, target = "_blank" } = Astro.props;
---
<a
class="text-caperren-green border-caperren-green hover:border-caperren-green-light hover:text-caperren-green-light rounded-2xl border-2 bg-black p-2"
href={href}
target={target}
>
{title}
</a>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
---
interface Props {
videoPath: string;
videoType?: string;
controls?: boolean;
autoPlay?: boolean;
loop?: boolean;
playsInline?: boolean;
}
const {
videoPath,
videoType = "video/mp4",
controls = true,
autoPlay = false,
loop = false,
playsInline = false,
} = Astro.props;
---
<div class="mx-auto my-auto">
<video
class="h-auto w-full"
controls={controls}
autoplay={autoPlay}
loop={loop}
playsinline={playsInline}
>
<source src={videoPath} type={videoType} />
Your browser does not support the video tag.
</video>
</div>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
---
---
<div class="">
<slot />
</div>

View File

@@ -0,0 +1,7 @@
---
---
<div class="space-y-2">
<slot />
</div>

View File

@@ -0,0 +1,43 @@
---
import { v4 as uuidv4 } from "uuid";
const popoverUUID = uuidv4();
const keys: { [key: string]: string } = {
ADCP: "Acoustic doppler current profiler",
COTS: "Consumer off-the-shelf",
CTD: "Conductivity, temperature, and depth sensor",
};
const key: string | undefined = Astro.props.key;
let word: string | undefined = Astro.props.word;
let definition: string | undefined = Astro.props.definition;
if (key && keys.hasOwnProperty(key)) {
word = key;
definition = keys[key];
}
---
<>
<a
href="#"
class="text-fg-brand decoration-caperren-green font-medium underline decoration-dashed hover:no-underline"
data-popover-target={popoverUUID}>{word}</a
>
<div
data-popover
id={popoverUUID}
role="tooltip"
class="text-body border-caperren-green invisible absolute z-90 inline-block w-fit max-w-96 rounded-lg border border-dashed bg-black p-3 text-sm opacity-0 shadow-xs transition-opacity duration-300"
>
<div>
<h3
class="text-heading border-b-caperren-green-dark mb-1 border-b font-semibold"
>
{word}
</h3>
<p>{definition}</p>
</div>
</div>
</>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

8
src/env.d.ts vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,138 @@
--- ---
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
import H2 from "@components/CustomHtmlWrappers/H2.astro";
import H3 from "@components/CustomHtmlWrappers/H3.astro";
import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import Paragraph from "@components/Paragraph.astro";
import Timeline from "@components/Timeline/Timeline.astro";
import type { carouselGroup } from "@interfaces/image-carousel.ts";
import { deploymentTimeline } from "./osu-ceoas-ocean-mixing-group.ts";
import building from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/building.jpg";
import glacier_selfie from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/glacier-selfie.jpg";
import ground_station from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/ground-station.jpg";
import iced_in from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/iced-in.jpg";
import pushing_icebergs from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/pushing-icebergs.jpg";
import ross_at_terminus from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/ross-at-terminus.png";
import ross_on_the_docks from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/ross-on-the-docks.jpg";
import ross_steller_ice_operations from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/ross-steller-ice-operations.png";
import steller_at_terminus from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/steller-at-terminus.png";
import steller_in_ice_from_above from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/steller-in-ice-from-above.jpg";
import whole_glacier from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/whole-glacier.jpg";
import working_at_the_docks from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/working-at-the-docks.jpg";
import working_trailer from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/working-trailer.jpg";
import massive_calving from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/videos/massive-calving-compressed.mp4";
import petersburg_working_timelapse from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/videos/petersburg-working-timelapse-compressed.mp4";
import ross_iceops from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/videos/ross-iceops-compressed.mp4";
import ross_terminus_calving from "@assets/experience/osu-ceoas-ocean-mixing-group/leconte-glacier-deployments/videos/ross-terminus-calving-compressed.mp4";
const videos = [
ross_terminus_calving,
ross_iceops,
massive_calving,
petersburg_working_timelapse,
];
const headerCarouselGroup: carouselGroup = {
animation: "slide",
images: [
ross_at_terminus,
ross_steller_ice_operations,
steller_at_terminus,
steller_in_ice_from_above,
whole_glacier,
ground_station,
glacier_selfie,
iced_in,
pushing_icebergs,
ross_on_the_docks,
working_at_the_docks,
building,
working_trailer,
],
};
import InlineLink from "@components/InlineLink.astro";
import Video from "@components/Media/Video.astro";
import Paragraphs from "@components/Paragraphs.astro";
import PopoverWordDefinition from "@components/PopoverWordDefinition.astro";
import { subTitles } from "./osu-ceoas-ocean-mixing-group.ts";
--- ---
<ExperienceLayout title="CEOAS - LeConte Glacier Deployments"> <ExperienceLayout title="LeConte Glacier Deployments" subTitles={subTitles}>
</ExperienceLayout> <Carousel carouselGroup={headerCarouselGroup} />
<H2 text="Summary" />
<H3 text="Timeline" />
<Timeline timeline={deploymentTimeline} />
<H3 text="Location" />
<iframe
class="w-full"
width="600"
height="450"
src="https://maps.google.com/maps?q=leconte%20glacier&t=k&z=11&ie=UTF8&iwloc=B&output=embed"
></iframe>
<H2 text="Details" />
<Paragraphs>
<Paragraph>
As part of my time working on the
<InlineLink
href="/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler"
>
Robotic Oceanographic Surface Sampler</InlineLink
>, I had the fantastic opportunity to be deployed at the LeConte Glacier
in Alaska! This started in early 2017 with setup and ocean trials in
nearby Petersburg. The team had sent multiple shipping containers with our
robotic platforms and most equipment to assemble, test, and debug them a
few months prior, allowing us to get to work the moment we arrived. We
spent multiple weeks at the docks with our makeshift workstations built
from plywood and pelican cases, validating the hardware we'd sent, and
making adjustments with improved hardware we'd hand-carried on our
flights. This also provided a great opportunity to work out any final
firmware and/or software bugs while the vehicles were still relatively
easy to retrieve. After a short trip back home to recover, and prep any
last minute items we'd forgotten, our research team flew back and headed
for the glacier!
</Paragraph>
<Paragraph>
The towering mountain of ice sits roughly 30 miles from Petersburg, so
we'd commissioned an off-season fishing vessel, Steller, and it's crew, to
take us as close to it as was reasonably safe. The team worked 24 hours a
day, on two shifts, deploying and retrieving the ROSS platforms,
performing repairs (as needed), recovering/processing collected data,
manually deploying the ship's <PopoverWordDefinition key="CTD" />, and
occasionally spending considerable time pushing icebergs the size of
houses away from an <PopoverWordDefinition key="ADCP" /> mounted to Steller
using fiberglass poles. Many hardware failures had to be solved during these
long days, and it was a very rewarding and creative experience to work around
the limitations of this isolated (and salty) environment.
</Paragraph>
<Paragraph>
On top of being a unique engineering and team building experience, LeConte
lives among the most beautiful places I've yet to experience in this life.
There's something special about being somewhere so incredibly remote and
untouched by humans. The pristine evergreen forests, eerie blue-green hues
of the glacier and icebergs, ancient towering mountains, and genuinely
curious looks from local land and marine life unfamiliar with human
presence made it humbly clear that for once we as humans were the odd ones
out. These trips were ones that I will treasure and think back on fondly
on for the rest of my life.
</Paragraph>
</Paragraphs>
<H2 text="Videos" />
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{
videos.map((video) => (
<Video
videoPath={video}
autoPlay={true}
loop={true}
playsInline={true}
/>
))
}
</div>
</ExperienceLayout>

View File

@@ -0,0 +1,29 @@
import type { timelineEntry } from "@interfaces/timeline.ts";
export const deploymentTimeline: timelineEntry[] = [
{
event: "Setup & Ocean Trials",
eventDetail: "Petersburg, AK",
date: "April 2017",
},
{
event: "Glacier Deployment #1",
eventDetail: "LeConte Glacier, AK",
date: "May 2017",
},
{
event: "Glacier Deployment #2",
eventDetail: "LeConte Glacier, AK",
date: "September 2017",
},
{
event: "Scientific Paper Published",
eventDetail: "The Oceanographic Society",
date: "September 2017",
},
];
export const subTitles = [
"Oregon State University",
"College of Earth, Ocean, and Atmospheric Sciences",
];

View File

@@ -1,6 +1,112 @@
--- ---
import ExperienceLayout from "@layouts/ExperienceLayout.astro"; import ExperienceLayout from "@layouts/ExperienceLayout.astro";
import H2 from "@components/CustomHtmlWrappers/H2.astro";
import H3 from "@components/CustomHtmlWrappers/H3.astro";
import LinkButton from "@components/LinkButton.astro";
import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import PdfViewer from "@components/Media/PdfViewer.astro";
import PopoverWordDefinition from "@components/PopoverWordDefinition.astro";
import Timeline from "@components/Timeline/Timeline.astro";
import type { carouselGroup } from "@interfaces/image-carousel.ts";
import type { timelineEntry } from "@interfaces/timeline.ts";
import electronics_box from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler/electronics-box.jpg";
import jet_drive from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler/jet-drive.jpg";
import ross_on_vessel_at_night from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler/ross-on-vessel-at-night.jpg";
import ross_on_vessel from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler/ross-on-vessel.jpg";
import publication from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler/ross-publication.pdf";
import ross_team from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler/ross-team.jpg";
import ui from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler/ui.jpg";
import {
deploymentTimeline,
subTitles,
} from "./osu-ceoas-ocean-mixing-group.ts";
const headerCarouselGroup: carouselGroup = {
animation: "slide",
images: [
ross_team,
ross_on_vessel,
ross_on_vessel_at_night,
electronics_box,
jet_drive,
ui,
],
};
const timeline: timelineEntry[] = [
{
event: "Started",
eventDetail: "Joined ROSS",
date: "April 2016",
},
...deploymentTimeline,
{
event: "Finished",
eventDetail: "Left ROSS",
date: "May 2018",
},
];
--- ---
<ExperienceLayout title="CEOAS - Robotic Oceanographic Surface Sampler"> <ExperienceLayout
</ExperienceLayout> title="Robotic Oceanographic Surface Sampler"
subTitles={subTitles}
>
<Carousel carouselGroup={headerCarouselGroup} />
<div class="mt-4 flex items-center justify-center">
<LinkButton
href="https://tos.org/oceanography/article/autonomous-ctd-profiling-from-the-robotic-oceanographic-surface-sampler"
title="Official Scientific Publication"
/>
</div>
<H2 text="Summary" />
<H3 text="Timeline" />
<Timeline timeline={timeline} />
<H3 text="Key Takeaways" />
<ul class="list-inside list-disc">
<li>
<div class="inline-block">
Assembled, fabricated, and debugged both custom and
<PopoverWordDefinition key="COTS" />
hardware and electronics.
</div>
</li>
<li>Two</li>
<li>Three</li>
</ul>
<h3 class="my-4 font-bold md:text-lg">Skills Used</h3>
<div
class="border-caperren-green relative grid grid-flow-row gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
<div>
<div class="text-sm font-extrabold">Software</div>
<hr class="text-caperren-green" />
<ul class="list-inside list-disc text-sm">
<li>One</li>
<li>Two</li>
</ul>
</div>
<div>
<div class="text-sm font-extrabold">Electrical</div>
<hr class="text-caperren-green" />
</div>
<div>
<div class="text-sm font-extrabold">Mechanical</div>
<hr class="text-caperren-green" />
</div>
<div>
<div class="text-sm font-extrabold">Other</div>
<hr class="text-caperren-green" />
</div>
</div>
<H2 text="Details" />
power and voltage logging
<H2 text="Official Scientific Publication" />
<div class="h-334">
<PdfViewer pdf={publication} />
</div>
</ExperienceLayout>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More