Compare commits
21 Commits
6305482fc1
...
817c6076dc
| Author | SHA1 | Date | |
|---|---|---|---|
| 817c6076dc | |||
| 4847e9f172 | |||
| 3aa75e1a10 | |||
| 91cd9af0f8 | |||
| 4a59e44716 | |||
| 67eb549ed2 | |||
| 8fd009ffda | |||
| 4b1eb3777f | |||
| 7858d95f58 | |||
| b91d37db45 | |||
| 109996989e | |||
| 96151d6512 | |||
| ac266b98ec | |||
| 8fa3c0b3ab | |||
| cef1b3381f | |||
| 049861c255 | |||
| ba948e4181 | |||
| 68b6d7f785 | |||
| 1f9264a409 | |||
| 7774e31c36 | |||
| c9f921ba5b |
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
9
.gitignore
vendored
@@ -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
@@ -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
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
31
Dockerfile
@@ -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"]
|
|
||||||
63
Makefile
@@ -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)
|
||||||
|
|||||||
49
README.md
@@ -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
|
|
||||||
```
|
|
||||||
|
|
||||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
|
|
||||||
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
|
|
||||||
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
|
|
||||||
|
|
||||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 🚀 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).
|
|
||||||
|
|||||||
@@ -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: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [
|
||||||
|
// @ts-ignore
|
||||||
|
tailwindcss(),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
23
cspell.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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;'
|
|
||||||
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +12,7 @@ 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. */
|
||||||
@@ -22,42 +22,42 @@ export default defineConfig({
|
|||||||
/* 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 */
|
/* Configure projects for major browsers */
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'chromium',
|
name: "chromium",
|
||||||
use: {...devices['Desktop Chrome']},
|
use: { ...devices["Desktop Chrome"] },
|
||||||
},
|
},
|
||||||
|
|
||||||
// {
|
{
|
||||||
// name: 'firefox',
|
name: "firefox",
|
||||||
// use: { ...devices['Desktop Firefox'] },
|
use: { ...devices["Desktop Firefox"] },
|
||||||
// },
|
},
|
||||||
//
|
|
||||||
// {
|
{
|
||||||
// name: 'webkit',
|
name: "webkit",
|
||||||
// use: { ...devices['Desktop Safari'] },
|
use: { ...devices["Desktop Safari"] },
|
||||||
// },
|
},
|
||||||
|
|
||||||
/* Test against mobile viewports. */
|
/* Test against mobile viewports. */
|
||||||
{
|
{
|
||||||
name: 'Mobile Chrome',
|
name: "Mobile Chrome",
|
||||||
use: {...devices['Pixel 5']},
|
use: { ...devices["Pixel 5"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mobile Safari",
|
||||||
|
use: { ...devices["iPhone 12"] },
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// name: 'Mobile Safari',
|
|
||||||
// use: { ...devices['iPhone 12'] },
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Test against branded browsers. */
|
/* Test against branded browsers. */
|
||||||
// {
|
// {
|
||||||
@@ -72,8 +72,8 @@ export default defineConfig({
|
|||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run preview',
|
command: "npm run preview",
|
||||||
url: 'http://localhost:4321',
|
url: "http://localhost:4321",
|
||||||
timeout: 120 * 1000,
|
timeout: 120 * 1000,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
|
|||||||
51
project-words.txt
Normal 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
|
After Width: | Height: | Size: 308 B |
BIN
public/180x180-favicon.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/192x192-favicon.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/32x32-favicon.png
Normal file
|
After Width: | Height: | Size: 500 B |
BIN
public/48x48-favicon.png
Normal file
|
After Width: | Height: | Size: 785 B |
BIN
public/512x512-favicon.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 662 B |
BIN
src/assets/about/headshot.jpg
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 956 KiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 4.1 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 5.1 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 4.1 MiB |
|
After Width: | Height: | Size: 5.1 MiB |
|
After Width: | Height: | Size: 6.8 MiB |
|
After Width: | Height: | Size: 5.0 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 851 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 902 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
@@ -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 |
@@ -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"/>
|
|
||||||
@@ -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)
|
|
||||||
5
src/components/CustomHtmlWrappers/H2.astro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<h2 class="my-4 font-bold md:text-2xl">{Astro.props.text}</h2>
|
||||||
5
src/components/CustomHtmlWrappers/H3.astro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<h3 class="mt-4 mb-2 font-bold md:text-lg">{Astro.props.text}</h3>
|
||||||
@@ -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">
|
---
|
||||||
|
|
||||||
|
<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_BUILD_ENVIRONMENT || "development"}</span>
|
||||||
<span>{import.meta.env.PUBLIC_REPO_VERSION_HASH || "invalid"}</span>
|
<span>{import.meta.env.PUBLIC_REPO_VERSION_HASH || "invalid"}</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
14
src/components/InlineLink.astro
Normal 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>
|
||||||
|
</>
|
||||||
18
src/components/LinkButton.astro
Normal 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>
|
||||||
145
src/components/Media/CustomCarousel/CustomCarousel.astro
Normal 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>
|
||||||
109
src/components/Media/CustomCarousel/custom-carousel.ts
Normal 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);
|
||||||
7
src/components/Media/PdfViewer.astro
Normal 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>
|
||||||
34
src/components/Media/Video.astro
Normal 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>
|
||||||
13
src/components/Media/YtVideo.astro
Normal 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>
|
||||||
@@ -6,28 +6,42 @@ 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 src={logo_title_large}
|
<Image
|
||||||
height="56"
|
src={logo_title_large}
|
||||||
width="139"
|
class="h-10 w-auto lg:h-14"
|
||||||
class="h-14 w-auto"
|
|
||||||
alt="logo title"
|
alt="logo title"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<button data-collapse-toggle="navbar-multi-level" type="button"
|
<button
|
||||||
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm md:hidden focus:ring-2 focus:ring-caperren-green"
|
data-collapse-toggle="navbar-multi-level"
|
||||||
aria-controls="navbar-multi-level" aria-expanded="false">
|
type="button"
|
||||||
|
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"
|
||||||
|
>
|
||||||
<span class="sr-only">Open main menu</span>
|
<span class="sr-only">Open main menu</span>
|
||||||
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
|
<svg
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
class="h-5 w-5"
|
||||||
d="M1 1h15M1 7h15M1 13h15"/>
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 17 14"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M1 1h15M1 7h15M1 13h15"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="z-40 hidden w-full md:block md:w-auto" id="navbar-multi-level">
|
<div
|
||||||
|
class="z-40 mt-1 hidden w-full lg:block lg:w-auto"
|
||||||
|
id="navbar-multi-level"
|
||||||
|
>
|
||||||
<NestedNavbarEntry items={siteLayout} />
|
<NestedNavbarEntry items={siteLayout} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,46 +6,73 @@ 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")}>
|
|
||||||
|
<ul
|
||||||
|
class={"flex flex-col p-4 bg-black border-caperren-green " +
|
||||||
|
(depth ? "space-y-2" : "items-start lg:flex-row lg:space-x-8 lg:mt-0 ")}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
items.map((entry, index) => (
|
items.map(
|
||||||
|
(entry) =>
|
||||||
(entry.enabled ?? true) && (
|
(entry.enabled ?? true) && (
|
||||||
<li>
|
<li>
|
||||||
{Array.isArray(entry.children) && entry.children.length ? (
|
{Array.isArray(entry.children) && entry.children.length ? (
|
||||||
<button id={"dropdownNavbarLink" + getNavLinkSuffix(entry)}
|
<div>
|
||||||
data-dropdown-toggle={"dropdownNavbar" + getNavLinkSuffix(entry)}
|
<button
|
||||||
|
id={"dropdownNavbarLink" + getNavLinkSuffix(entry)}
|
||||||
|
data-dropdown-toggle={
|
||||||
|
"dropdownNavbar" + getNavLinkSuffix(entry)
|
||||||
|
}
|
||||||
data-dropdown-placement="bottom"
|
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 ">
|
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}
|
{entry.navText}
|
||||||
<svg class="w-2.5 h-2.5 ms-2.5" aria-hidden="true"
|
<svg
|
||||||
|
class="ms-2.5 h-2.5 w-2.5"
|
||||||
|
aria-hidden="true"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none" viewBox="0 0 10 6">
|
viewBox="0 0 10 6"
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="m1 1 4 4 4-4"/>
|
d="m1 1 4 4 4-4"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<div
|
||||||
<div id={"dropdownNavbar" + getNavLinkSuffix(entry)}
|
id={"dropdownNavbar" + getNavLinkSuffix(entry)}
|
||||||
class="z-10 hidden bg-black border border-caperren-green shadow-sm w-screen md:w-max">
|
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}/>
|
<Astro.self
|
||||||
|
items={entry.children}
|
||||||
|
paths={[...paths, entry.path]}
|
||||||
|
depth={depth + 1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
|
<a
|
||||||
<a href={getHrefPath(entry)}
|
href={getHrefPath(entry)}
|
||||||
class="block py-2 px-3 bg-transparent hover:text-caperren-green-light ring-caperren-green-dark md:p-0"
|
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>
|
aria-current="page"
|
||||||
|
>
|
||||||
|
{entry.navText}
|
||||||
|
</a>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
),
|
||||||
)
|
)
|
||||||
))}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
7
src/components/Paragraph.astro
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
7
src/components/Paragraphs.astro
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
43
src/components/PopoverWordDefinition.astro
Normal 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>
|
||||||
|
</>
|
||||||
@@ -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 => (
|
{
|
||||||
|
data.header.map((headingText) => (
|
||||||
<th scope="col" class={paddingClasses}>
|
<th scope="col" class={paddingClasses}>
|
||||||
{headingText}
|
{headingText}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.rows.map(row => (
|
{
|
||||||
<tr class=" border-b dark:bg-black border-caperren-green">
|
data.rows.map((row) => (
|
||||||
{row.map(rowColumnText => (
|
<tr class="border-caperren-green border-b dark:bg-black">
|
||||||
<th scope="row"
|
{row.map((rowColumnText) => (
|
||||||
class={paddingClasses + " font-medium whitespace-nowrap"}>
|
<th
|
||||||
|
scope="row"
|
||||||
|
class={paddingClasses + " font-medium whitespace-nowrap"}
|
||||||
|
>
|
||||||
{rowColumnText}
|
{rowColumnText}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -5,30 +5,35 @@ 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"
|
>
|
||||||
|
{
|
||||||
|
timeline.map((entry, index) => (
|
||||||
|
<div
|
||||||
|
class="border-caperren-green min-w-s max-w-s rounded-lg border bg-black px-2 pt-1 pb-2"
|
||||||
data-timeline-node-index={index}
|
data-timeline-node-index={index}
|
||||||
>
|
>
|
||||||
<h3 class="text-lg font-bold">
|
<h3 class="text-lg font-bold">{entry.event}</h3>
|
||||||
{entry.event}
|
<h4 class="leading-none font-semibold">{entry.eventDetail}</h4>
|
||||||
</h3>
|
<time class="mt-1 mb-2 text-sm leading-none italic">
|
||||||
<h4 class="font-semibold leading-none">
|
|
||||||
{entry.eventDetail}
|
|
||||||
</h4>
|
|
||||||
<time class="mb-2 mt-1 text-sm italic leading-none">
|
|
||||||
{entry.date}
|
{entry.date}
|
||||||
</time>
|
</time>
|
||||||
{entry.description && (
|
{entry.description && (
|
||||||
<p class="text-sm font-normal">
|
<p class="text-sm font-normal">{entry.description}</p>
|
||||||
{entry.description}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="z-0 w-full max-w-full overflow-x-visible"
|
||||||
|
data-custom-timeline-line-wrapper
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</custom-timeline>
|
</custom-timeline>
|
||||||
|
|
||||||
<script src="./timeline.ts"/>
|
<script src="./timeline.ts"></script>
|
||||||
|
|||||||
@@ -5,40 +5,76 @@ class CustomTimeline extends HTMLElement {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._eventElements = this._getNodeElements();
|
this._eventElements = this._getNodeElements();
|
||||||
|
|
||||||
|
|
||||||
window.addEventListener("load", this._paintLeaderLines);
|
window.addEventListener("load", this._paintLeaderLines);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_getNodeElements = (): Element[] =>
|
_getNodeElements = (): Element[] =>
|
||||||
Array.from(this.querySelectorAll('[data-timeline-node-index]')).sort(
|
Array.from(this.querySelectorAll("[data-timeline-node-index]")).sort(
|
||||||
(elementA, elementB) =>
|
(elementA, elementB) =>
|
||||||
Number(elementA.getAttribute('data-timeline-node-index')) - Number(elementB.getAttribute('data-timeline-node-index'))
|
Number(elementA.getAttribute("data-timeline-node-index")) -
|
||||||
|
Number(elementB.getAttribute("data-timeline-node-index")),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
_paintLeaderLines = () => {
|
_paintLeaderLines = () => {
|
||||||
let pairs = this._eventElements.map((entry, index) => {
|
let pairs = this._eventElements
|
||||||
|
.map((entry, index) => {
|
||||||
if (index < this._eventElements.length - 1) {
|
if (index < this._eventElements.length - 1) {
|
||||||
return [entry, this._eventElements[index + 1]];
|
return [entry, this._eventElements[index + 1]];
|
||||||
}
|
}
|
||||||
}).filter(pair => pair !== undefined)
|
})
|
||||||
|
.filter((pair) => pair !== undefined);
|
||||||
|
|
||||||
pairs.forEach(pair => {
|
let contentBodyScrolling = document.getElementById(
|
||||||
new LeaderLine({
|
"content-body-scrolling",
|
||||||
|
);
|
||||||
|
let wrapper = this.querySelector(
|
||||||
|
"[data-custom-timeline-line-wrapper]",
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
const position = (line: LeaderLine) => {
|
||||||
|
wrapper.style.transform = "none";
|
||||||
|
let rectWrapper = wrapper.getBoundingClientRect();
|
||||||
|
|
||||||
|
wrapper.style.transform =
|
||||||
|
"translate(-" +
|
||||||
|
Number(rectWrapper.left) +
|
||||||
|
"px, -" +
|
||||||
|
Number(rectWrapper.top) +
|
||||||
|
"px)";
|
||||||
|
|
||||||
|
line.position();
|
||||||
|
};
|
||||||
|
|
||||||
|
pairs.forEach((pair) => {
|
||||||
|
let line = new LeaderLine({
|
||||||
start: pair[0],
|
start: pair[0],
|
||||||
end: pair[1],
|
end: pair[1],
|
||||||
color: '#10ac25',
|
color: "#10ac25",
|
||||||
size: 3,
|
size: 3,
|
||||||
startSocket: "right",
|
startSocket: "right",
|
||||||
endSocket: "left",
|
endSocket: "left",
|
||||||
startPlug: "square",
|
startPlug: "square",
|
||||||
endPlug: "arrow3"
|
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);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
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" },
|
||||||
{
|
{
|
||||||
enabled: false,
|
|
||||||
navText: "Experiences",
|
navText: "Experiences",
|
||||||
path: "experience",
|
path: "experience",
|
||||||
children: [
|
children: [
|
||||||
@@ -16,18 +15,17 @@ export const siteLayout: navLink[] = [
|
|||||||
{
|
{
|
||||||
enabled: false,
|
enabled: false,
|
||||||
navText: "Hardware Test Engineer I/II",
|
navText: "Hardware Test Engineer I/II",
|
||||||
path: "hardware-test-engineer-i-ii"
|
path: "hardware-test-engineer-i-ii",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: false,
|
enabled: false,
|
||||||
navText: "Avionics Test Engineering Internship",
|
navText: "Avionics Test Engineering Internship",
|
||||||
path: "avionics-test-engineering-internship"
|
path: "avionics-test-engineering-internship",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: false,
|
navText: "OSU CEOAS Ocean Mixing Group",
|
||||||
navText: "OSU CEOAS",
|
|
||||||
path: "osu-ceoas-ocean-mixing-group",
|
path: "osu-ceoas-ocean-mixing-group",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -36,11 +34,10 @@ export const siteLayout: navLink[] = [
|
|||||||
path: "robotic-oceanographic-surface-sampler",
|
path: "robotic-oceanographic-surface-sampler",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: false,
|
|
||||||
navText: "LeConte Glacier Deployments",
|
navText: "LeConte Glacier Deployments",
|
||||||
path: "leconte-glacier-deployments",
|
path: "leconte-glacier-deployments",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -76,8 +73,8 @@ export const siteLayout: navLink[] = [
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
navText: "ZScan Processor",
|
navText: "ZScan Processor",
|
||||||
path: "zscan-processor",
|
path: "zscan-processor",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -103,25 +100,41 @@ export const siteLayout: navLink[] = [
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
navText: "Club Officer",
|
navText: "Club Officer",
|
||||||
path: "club-officer",
|
path: "club-officer",
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
navText: "Hobbies",
|
navText: "Hobbies",
|
||||||
path: "hobby",
|
path: "hobby",
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
{
|
{
|
||||||
enabled: false,
|
enabled: false,
|
||||||
navText: "Homelab", path: "homelab",
|
navText: "Homelab",
|
||||||
|
path: "homelab",
|
||||||
children: [
|
children: [
|
||||||
{enabled: false, navText: "Home Server Rack", path: "home-server-rack"},
|
{
|
||||||
{enabled: false, navText: "Offsite Backup Rack", path: "offsite-backup-rack"},
|
enabled: false,
|
||||||
{enabled: false, navText: "Kubernetes Cluster", path: "kubernetes-cluster"},
|
navText: "Home Server Rack",
|
||||||
{enabled: false, navText: "Home Automation", path: "home-automation"},
|
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",
|
navText: "Motorcycling",
|
||||||
@@ -132,52 +145,82 @@ export const siteLayout: navLink[] = [
|
|||||||
navText: "Custom Accessories",
|
navText: "Custom Accessories",
|
||||||
path: "custom-accessories",
|
path: "custom-accessories",
|
||||||
children: [
|
children: [
|
||||||
{navText: "Chubby Buttons 2 Mount", path: "chubby-buttons-2-mount"},
|
{
|
||||||
]
|
navText: "Chubby Buttons 2 Mount",
|
||||||
|
path: "chubby-buttons-2-mount",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: false,
|
enabled: false,
|
||||||
navText: "Trips",
|
navText: "Trips",
|
||||||
path: "trips",
|
path: "trips",
|
||||||
children: [
|
children: [
|
||||||
{navText: "2025-08 | Alaska ", path: "2025-08-alaska", enabled: false,},
|
{
|
||||||
{navText: "2024-10 | Norway ", path: "2024-10-norway", enabled: false,}
|
navText: "2025-08 | Alaska ",
|
||||||
]
|
path: "2025-08-alaska",
|
||||||
|
enabled: false,
|
||||||
},
|
},
|
||||||
]
|
{
|
||||||
|
navText: "2024-10 | Norway ",
|
||||||
|
path: "2024-10-norway",
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: false,
|
enabled: false,
|
||||||
navText: "Projects",
|
navText: "Projects",
|
||||||
path: "projects",
|
path: "projects",
|
||||||
children: [
|
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 },
|
{ navText: "Shed Solar", path: "shed-solar", enabled: false },
|
||||||
{navText: "OSSM Overkill Edition", path: "ossm-overkill-edition", enabled: false},
|
],
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{ enabled: false, navText: "NixOS", path: "nixos" },
|
{ enabled: false, navText: "NixOS", path: "nixos" },
|
||||||
{ navText: "Body Mods", path: "body-mods" },
|
{ navText: "Body Mods", path: "body-mods" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
navText: "Resumes",
|
navText: "Resumes",
|
||||||
path: "resume",
|
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: "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: "Github", pubpath: "https://github.com/caperren" },
|
||||||
{navText: "LinkedIn", pubpath: "https://www.linkedin.com/in/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;
|
||||||
@@ -200,12 +243,12 @@ export const pathToMetadata = (path: string): navLink => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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[] = [];
|
||||||
|
|
||||||
@@ -213,8 +256,12 @@ export const getPaths = (
|
|||||||
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,
|
||||||
|
[...paths, currentEntry.path || ""],
|
||||||
|
disabledOnly,
|
||||||
|
),
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
let enabled = currentEntry.enabled ?? true;
|
let enabled = currentEntry.enabled ?? true;
|
||||||
if (disabledOnly ? !enabled : enabled) {
|
if (disabledOnly ? !enabled : enabled) {
|
||||||
@@ -223,4 +270,4 @@ export const getPaths = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [...new Set(foundPaths)];
|
return [...new Set(foundPaths)];
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface videoConfig {
|
export interface videoConfig {
|
||||||
videoTitle?: string
|
videoTitle?: string;
|
||||||
videoPath: string;
|
videoPath: string;
|
||||||
videoType?: string;
|
videoType?: string;
|
||||||
}
|
}
|
||||||
@@ -1,35 +1,79 @@
|
|||||||
---
|
---
|
||||||
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="icon" href="/48x48-favicon.png" type="image/x-icon" />
|
||||||
|
<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" />
|
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{pageEnabled ? pageTitle : "Corwin Perren"}</title>
|
<title>{pageEnabled ? pageTitle : "Corwin Perren"}</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-black text-white">
|
<body class="flex h-dvh w-full max-w-full flex-col bg-black text-white">
|
||||||
|
<div
|
||||||
|
id="content-body-scrolling"
|
||||||
|
class="grow overflow-x-hidden overflow-y-scroll"
|
||||||
|
>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<main class="mx-6 mt-6 mb-14">
|
<main class="mx-6 my-6">
|
||||||
{(Astro.props.title && showTitle && pageEnabled) && (
|
{
|
||||||
<h1 class="font-extrabold md:text-3xl md:mb-6">{Astro.props.title}</h1>
|
title && showTitle && pageEnabled && (
|
||||||
)}
|
<h1
|
||||||
{pageEnabled && (
|
class={
|
||||||
<slot/>
|
"text-xl font-extrabold md:text-3xl " +
|
||||||
)}
|
(subTitles ? "" : "md:mb-6")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{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>
|
</main>
|
||||||
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
<script src="../scripts/main.ts"></script>
|
<script src="../scripts/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from './BaseLayout.astro';
|
import BaseLayout from "./BaseLayout.astro";
|
||||||
---
|
---
|
||||||
<BaseLayout title={Astro.props.title}>
|
|
||||||
|
<BaseLayout {...Astro.props}>
|
||||||
<slot />
|
<slot />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from './BaseLayout.astro';
|
import BaseLayout from "./BaseLayout.astro";
|
||||||
---
|
---
|
||||||
<BaseLayout title={Astro.props.title}>
|
|
||||||
|
<BaseLayout {...Astro.props}>
|
||||||
<slot />
|
<slot />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
@@ -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}>
|
||||||
---
|
|
||||||
<BaseLayout title={Astro.props.title}>
|
|
||||||
<div class="h-dvh">
|
<div class="h-dvh">
|
||||||
<iframe src={resume} class="mx-auto w-9/10 md:w-3/5 h-7/10 md:h-4/5"/>
|
<PdfViewer class="mx-auto" pdf={Astro.props.resume} />
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
@@ -1,35 +1,31 @@
|
|||||||
---
|
---
|
||||||
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",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -101,17 +97,21 @@ const courseTable: tableData = {
|
|||||||
["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"
|
||||||
|
>Coursework Archives</a
|
||||||
|
>
|
||||||
|
<h3 class="my-4 font-bold underline md:text-lg">Course Listing</h3>
|
||||||
<Table data={courseTable} />
|
<Table data={courseTable} />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
@@ -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}>
|
||||||
|
<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>
|
</ExperienceLayout>
|
||||||
@@ -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",
|
||||||
|
];
|
||||||
@@ -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
|
||||||
|
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>
|
</ExperienceLayout>
|
||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
---
|
---
|
||||||
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
|
||||||
import Carousel from "@components/CustomCarousel/CustomCarousel.astro";
|
import YtVideo from "@components/Media/YtVideo.astro";
|
||||||
import type { carouselGroup } from "@interfaces/image-carousel.ts";
|
import type { carouselGroup } from "@interfaces/image-carousel.ts";
|
||||||
import YtVideo from "@components/YtVideo.astro";
|
|
||||||
import type { videoConfig } from "@interfaces/video.ts";
|
import type { videoConfig } from "@interfaces/video.ts";
|
||||||
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
</h2>
|
||||||
|
<ul class="list-inside list-disc space-y-1">
|
||||||
<li>Clock</li>
|
<li>Clock</li>
|
||||||
<li>Event Timer</li>
|
<li>Event Timer</li>
|
||||||
<li>Status Indication
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Status Indication
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Rover Connection</li>
|
<li>Rover Connection</li>
|
||||||
<li>Controller Connection Info</li>
|
<li>Controller Connection Info</li>
|
||||||
<li>Radio Stats</li>
|
<li>Radio Stats</li>
|
||||||
@@ -44,16 +44,19 @@ const videoList: videoConfig[] = [
|
|||||||
<li>Camera Connections</li>
|
<li>Camera Connections</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Radio Direction Finding
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Radio Direction Finding
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Raw Radio RSSI Indication</li>
|
<li>Raw Radio RSSI Indication</li>
|
||||||
<li>Radio RSSI Pulse Frequency w/Validity Indication</li>
|
<li>Radio RSSI Pulse Frequency w/Validity Indication</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Arm
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Arm
|
||||||
<li>Special Movements
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
<li>
|
||||||
|
Special Movements
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Stow Arm</li>
|
<li>Stow Arm</li>
|
||||||
<li>Unstow Arm</li>
|
<li>Unstow Arm</li>
|
||||||
<li>Upright Arm</li>
|
<li>Upright Arm</li>
|
||||||
@@ -62,8 +65,9 @@ const videoList: videoConfig[] = [
|
|||||||
<li>Calibrate Arm</li>
|
<li>Calibrate Arm</li>
|
||||||
<li>Clear Arm Fault</li>
|
<li>Clear Arm Fault</li>
|
||||||
<li>Reset Arm Motor Drivers</li>
|
<li>Reset Arm Motor Drivers</li>
|
||||||
<li>Task Movements
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Task Movements
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Approach Oxygen Tank</li>
|
<li>Approach Oxygen Tank</li>
|
||||||
<li>Depart Oxygen Tank</li>
|
<li>Depart Oxygen Tank</li>
|
||||||
<li>Approach Light Beacon</li>
|
<li>Approach Light Beacon</li>
|
||||||
@@ -72,12 +76,14 @@ const videoList: videoConfig[] = [
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Mining/Science
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Mining/Science
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Bucket Weight Measurement</li>
|
<li>Bucket Weight Measurement</li>
|
||||||
<li>Bucket Lift/Tilt Position Readouts</li>
|
<li>Bucket Lift/Tilt Position Readouts</li>
|
||||||
<li>Preset Bucket Movements
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Preset Bucket Movements
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Mining Transport</li>
|
<li>Mining Transport</li>
|
||||||
<li>Mining Measure</li>
|
<li>Mining Measure</li>
|
||||||
<li>Mining Scoop</li>
|
<li>Mining Scoop</li>
|
||||||
@@ -86,26 +92,30 @@ const videoList: videoConfig[] = [
|
|||||||
<li>Mining Probe</li>
|
<li>Mining Probe</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Science Probe Readings
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Science Probe Readings
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Temp in C</li>
|
<li>Temp in C</li>
|
||||||
<li>Moisture %</li>
|
<li>Moisture %</li>
|
||||||
<li>Loss Tangent</li>
|
<li>Loss Tangent</li>
|
||||||
<li>Soil Electrical Conductivity</li>
|
<li>Soil Electrical Conductivity</li>
|
||||||
<li>Real Dielectric Permitivity</li>
|
<li>Real Dielectric Permittivity</li>
|
||||||
<li>Imaginary Dielectric Permitivity</li>
|
<li>Imaginary Dielectric Permittivity</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Science Camera Controls
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Science Camera Controls
|
||||||
<li>Video Output Selection
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
<li>
|
||||||
|
Video Output Selection
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Network Video</li>
|
<li>Network Video</li>
|
||||||
<li>Camera LCD</li>
|
<li>Camera LCD</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Photo Controls
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Photo Controls
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Zoom In One Step</li>
|
<li>Zoom In One Step</li>
|
||||||
<li>Zoom Out One Step</li>
|
<li>Zoom Out One Step</li>
|
||||||
<li>Full Zoom In</li>
|
<li>Full Zoom In</li>
|
||||||
@@ -117,12 +127,14 @@ const videoList: videoConfig[] = [
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>SSH Console
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
SSH Console
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>SSH Terminal Display</li>
|
<li>SSH Terminal Display</li>
|
||||||
<li>SSH Command Entry</li>
|
<li>SSH Command Entry</li>
|
||||||
<li>Preset Commands
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Preset Commands
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Network Host Scan</li>
|
<li>Network Host Scan</li>
|
||||||
<li>List Wifi Networks</li>
|
<li>List Wifi Networks</li>
|
||||||
<li>Equipment Login and Help</li>
|
<li>Equipment Login and Help</li>
|
||||||
@@ -135,98 +147,115 @@ const videoList: videoConfig[] = [
|
|||||||
<li>Connect/Disconnect Rover Wifi by SSID</li>
|
<li>Connect/Disconnect Rover Wifi by SSID</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Settings
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Settings
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Map Selection</li>
|
<li>Map Selection</li>
|
||||||
<li>Map Zoom Level</li>
|
<li>Map Zoom Level</li>
|
||||||
<li>Rover Wifi Radio Channel Selection</li>
|
<li>Rover Wifi Radio Channel Selection</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Mapping Display
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Mapping Display
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Shows Google Map Terrain</li>
|
<li>Shows Google Map Terrain</li>
|
||||||
<li>Shows Rover Location And Orientation</li>
|
<li>Shows Rover Location And Orientation</li>
|
||||||
<li>Shows Rover GPS Coordinates</li>
|
<li>Shows Rover GPS Coordinates</li>
|
||||||
<li>Shows Saved Waypoints</li>
|
<li>Shows Saved Waypoints</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Waypoint Entry / Editing
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Waypoint Entry / Editing
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Name Entry For Landmarks</li>
|
<li>Name Entry For Landmarks</li>
|
||||||
<li>GPS Entry in Decimal</li>
|
<li>GPS Entry in Decimal</li>
|
||||||
<li>GPS Entry in Degree/Minute/Second</li>
|
<li>GPS Entry in Degree/Minute/Second</li>
|
||||||
<li>Waypoint Color Choice</li>
|
<li>Waypoint Color Choice</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Navigation Waypoints
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Navigation Waypoints
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Shows And Allows Editing Of Nav Waypoints</li>
|
<li>Shows And Allows Editing Of Nav Waypoints</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Landmark Waypoints
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Landmark Waypoints
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Shows And Allows Editing Of Landmark Waypoints</li>
|
<li>Shows And Allows Editing Of Landmark Waypoints</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Arm Joint Positions
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Arm Joint Positions
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Positions Of Six Arm Joints In Revolutions</li>
|
<li>Positions Of Six Arm Joints In Revolutions</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Gripper Joint Positions
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Gripper Joint Positions
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Positions Shown As Raw Encoder Positions</li>
|
<li>Positions Shown As Raw Encoder Positions</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Arm Motor Drive Statuses
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Arm Motor Drive Statuses
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Communication/Movement/Fault Statuses For All Six Arm Joints</li>
|
<li>Communication/Movement/Fault Statuses For All Six Arm Joints</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Gripper Mode Readouts
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Gripper Mode Readouts
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Gripper Mode Control State</li>
|
<li>Gripper Mode Control State</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Xbox Control Mode
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Xbox Control Mode
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Showed Whether Xbox Controller Moving Arm Or Mining</li>
|
<li>Showed Whether Xbox Controller Moving Arm Or Mining</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Heading and Goal Indication w/Compass
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Heading and Goal Indication w/Compass
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Raw Heading Indication</li>
|
<li>Raw Heading Indication</li>
|
||||||
<li>Goal Indication (Unused)</li>
|
<li>Goal Indication (Unused)</li>
|
||||||
<li>Compass Heading Indication</li>
|
<li>Compass Heading Indication</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Low Resolution Mode
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Low Resolution Mode
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Controlled Low Resolution Fallback Mode During Radio Failure</li>
|
<li>Controlled Low Resolution Fallback Mode During Radio Failure</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Current Speed
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Current Speed
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>GPS Speed</li>
|
<li>GPS Speed</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Speed Limit
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Speed Limit
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>% Of Max Rover Speed As Limit</li>
|
<li>% Of Max Rover Speed As Limit</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Tank Drive Output
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Tank Drive Output
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>% Of Total Power To Left/Right Rover Drive Systems</li>
|
<li>% Of Total Power To Left/Right Rover Drive Systems</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>IMU Readings
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
IMU Readings
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>Pitch/Roll Readings In +/- 1 Readout</li>
|
<li>Pitch/Roll Readings In +/- 1 Readout</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Triple Camera Displays
|
<li>
|
||||||
<ul class="ps-5 space-y-1 list-disc list-inside">
|
Triple Camera Displays
|
||||||
|
<ul class="list-inside list-disc space-y-1 ps-5">
|
||||||
<li>One Primary Video Display</li>
|
<li>One Primary Video Display</li>
|
||||||
<li>Two Secondary Video Displays</li>
|
<li>Two Secondary Video Displays</li>
|
||||||
<li>Named Display For Currently Viewed Camera</li>
|
<li>Named Display For Currently Viewed Camera</li>
|
||||||
@@ -236,10 +265,15 @@ const videoList: videoConfig[] = [
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2 class="font-bold md:text-2xl my-4 underline">Rover Demos and Software Overviews</h2>
|
<h2 class="my-4 font-bold underline md:text-2xl">
|
||||||
{videoList.map((video) => (
|
Rover Demos and Software Overviews
|
||||||
<h3 class="font-bold md:text-lg my-4">{video.videoTitle}</h3>
|
</h2>
|
||||||
|
{
|
||||||
|
videoList.map((video) => (
|
||||||
|
<div>
|
||||||
|
<h3 class="my-4 font-bold md:text-lg">{video.videoTitle}</h3>
|
||||||
<YtVideo videoConfig={video} />
|
<YtVideo videoConfig={video} />
|
||||||
))}
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
</ExperienceLayout>
|
</ExperienceLayout>
|
||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -2,5 +2,4 @@
|
|||||||
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<ExperienceLayout title="SARL - Team Lead">
|
<ExperienceLayout title="SARL - Team Lead" />
|
||||||
</ExperienceLayout>
|
|
||||||
|
|||||||
@@ -2,5 +2,4 @@
|
|||||||
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<ExperienceLayout title="SARL - Zebrafish Embryo Pick and Plate">
|
<ExperienceLayout title="SARL - Zebrafish Embryo Pick and Plate" />
|
||||||
</ExperienceLayout>
|
|
||||||
|
|||||||
@@ -2,5 +2,4 @@
|
|||||||
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<ExperienceLayout title="SARL - ZScan Processor">
|
<ExperienceLayout title="SARL - ZScan Processor" />
|
||||||
</ExperienceLayout>
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
import ExperienceLayout from '@layouts/ExperienceLayout.astro';
|
import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
|
||||||
import Timeline from '@components/Timeline/Timeline.astro';
|
import Timeline from "@components/Timeline/Timeline.astro";
|
||||||
import Carousel from "@components/CustomCarousel/CustomCarousel.astro";
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
|
||||||
import spring_2019_interns from "@assets/experience/spacex/avionics-test-engineering-internship/spring-2019-interns.jpg";
|
import spring_2019_interns from "@assets/experience/spacex/avionics-test-engineering-internship/spring-2019-interns.jpg";
|
||||||
|
|
||||||
@@ -10,10 +10,8 @@ import type {timelineEntry} from "@interfaces/timeline.ts";
|
|||||||
|
|
||||||
const headerCarouselGroup: carouselGroup = {
|
const headerCarouselGroup: carouselGroup = {
|
||||||
animation: "slide",
|
animation: "slide",
|
||||||
images: [
|
images: [spring_2019_interns],
|
||||||
spring_2019_interns
|
};
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeline: timelineEntry[] = [
|
const timeline: timelineEntry[] = [
|
||||||
{
|
{
|
||||||
@@ -23,44 +21,60 @@ const timeline: timelineEntry[] = [
|
|||||||
{
|
{
|
||||||
event: "Finished",
|
event: "Finished",
|
||||||
date: "March 2019",
|
date: "March 2019",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<ExperienceLayout title="SpaceX - Avionics Test Engineering Internship">
|
<ExperienceLayout title="SpaceX - Avionics Test Engineering Internship">
|
||||||
<Carousel carouselGroup={headerCarouselGroup} />
|
<Carousel carouselGroup={headerCarouselGroup} />
|
||||||
|
|
||||||
<h2 class="font-bold md:text-2xl my-4">Summary</h2>
|
<h2 class="my-4 font-bold md:text-2xl">Summary</h2>
|
||||||
<h3 class="font-bold md:text-lg my-4">Timeline</h3>
|
<h3 class="my-4 font-bold md:text-lg">Timeline</h3>
|
||||||
<Timeline timeline={timeline} />
|
<Timeline timeline={timeline} />
|
||||||
<h3 class="font-bold md:text-lg my-4">Key Takeaways</h3>
|
<h3 class="my-4 font-bold md:text-lg">Key Takeaways</h3>
|
||||||
<ul class="list-disc list-inside">
|
<ul class="list-inside list-disc">
|
||||||
<li></li>
|
<li></li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 class="font-bold md:text-lg my-4">Skills Used</h3>
|
<h3 class="my-4 font-bold md:text-lg">Skills Used</h3>
|
||||||
<div class="relative grid gap-6 grid-flow-row sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 border-caperren-green">
|
<div
|
||||||
|
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>
|
||||||
<div class="font-extrabold text-sm">Software</div>
|
<div class="text-sm font-extrabold">Software</div>
|
||||||
<hr class="text-caperren-green" />
|
<hr class="text-caperren-green" />
|
||||||
<ul class="list-disc list-inside text-sm">
|
<ul class="list-inside list-disc text-sm">
|
||||||
<li>Python</li>
|
<li>Python</li>
|
||||||
<li>Test Driven Development</li>
|
<li>Test Driven Development</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-extrabold text-sm">Electrical</div>
|
<div class="text-sm font-extrabold">Electrical</div>
|
||||||
<hr class="text-caperren-green" />
|
<hr class="text-caperren-green" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-extrabold text-sm">Other</div>
|
<div class="text-sm font-extrabold">Other</div>
|
||||||
<hr class="text-caperren-green" />
|
<hr class="text-caperren-green" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="font-bold md:text-2xl my-4">Details</h2>
|
<h2 class="my-4 font-bold md:text-2xl">Details</h2>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Though I did get to work on some really fun projects during my internship at SpaceX, I unfortunately can’t go into much detail due to NDA’s and ITAR restrictions. What I can say is that I mainly wrote Python for a new avionics hardware test system. My experience with writing Python in the numerous other projects I’ve done really helped me out here, as the framework SpaceX has created was quite complex and would otherwise have been fairly difficult to write code for. I also wrote a simple tool for automating the creation of Jira work tickets so that the two teams that ended up using it wouldn’t have to have their members manually creating dozens of them as work and issues came in through a separate system.
|
|
||||||
|
|
||||||
I was also quite happy in that I got to perform some circuit debugging on avionics test system hardware, both for my project and for a separate test system. A final experience I had here was getting to work directly with the head engineer from a company that supplied a piece of test hardware I was interfacing with. It was quite incredible to see just how much weight a SpaceX email address had when trying to solve problems I had found with the hardware. Not only were they responsive, but in fact were willing to fast-track firmware updates for us to get things working. Coming from clubs and small labs where a support email might not even get a response for months, it was quite a refreshing experience.
|
|
||||||
|
|
||||||
|
Though I did get to work on some really fun projects during my internship at
|
||||||
|
SpaceX, I unfortunately can’t go into much detail due to NDA’s and ITAR
|
||||||
|
restrictions. What I can say is that I mainly wrote Python for a new avionics
|
||||||
|
hardware test system. My experience with writing Python in the numerous other
|
||||||
|
projects I’ve done really helped me out here, as the framework SpaceX has
|
||||||
|
created was quite complex and would otherwise have been fairly difficult to
|
||||||
|
write code for. I also wrote a simple tool for automating the creation of Jira
|
||||||
|
work tickets so that the two teams that ended up using it wouldn’t have to
|
||||||
|
have their members manually creating dozens of them as work and issues came in
|
||||||
|
through a separate system. I was also quite happy in that I got to perform
|
||||||
|
some circuit debugging on avionics test system hardware, both for my project
|
||||||
|
and for a separate test system. A final experience I had here was getting to
|
||||||
|
work directly with the head engineer from a company that supplied a piece of
|
||||||
|
test hardware I was interfacing with. It was quite incredible to see just how
|
||||||
|
much weight a SpaceX email address had when trying to solve problems I had
|
||||||
|
found with the hardware. Not only were they responsive, but in fact were
|
||||||
|
willing to fast-track firmware updates for us to get things working. Coming
|
||||||
|
from clubs and small labs where a support email might not even get a response
|
||||||
|
for months, it was quite a refreshing experience.
|
||||||
</ExperienceLayout>
|
</ExperienceLayout>
|
||||||
@@ -1,87 +1,98 @@
|
|||||||
---
|
---
|
||||||
import ExperienceLayout from '@layouts/ExperienceLayout.astro';
|
import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
|
||||||
import Timeline from '@components/Timeline/Timeline.astro';
|
import Timeline from "@components/Timeline/Timeline.astro";
|
||||||
import Carousel from "@components/CustomCarousel/CustomCarousel.astro";
|
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||||
|
|
||||||
import starlink_headquarters_selfie
|
import starlink_headquarters_selfie from "@assets/experience/spacex/hardware-test-engineer-i-ii/starlink-headquarters-selfie.jpg";
|
||||||
from "@assets/experience/spacex/hardware-test-engineer-i-ii/starlink-headquarters-selfie.jpg";
|
|
||||||
|
|
||||||
import type { carouselGroup } from "@interfaces/image-carousel.ts";
|
import type { carouselGroup } from "@interfaces/image-carousel.ts";
|
||||||
import type { timelineEntry } from "@interfaces/timeline.ts";
|
import type { timelineEntry } from "@interfaces/timeline.ts";
|
||||||
|
|
||||||
const headerCarouselGroup: carouselGroup = {
|
const headerCarouselGroup: carouselGroup = {
|
||||||
animation: "slide",
|
animation: "slide",
|
||||||
images: [
|
images: [starlink_headquarters_selfie],
|
||||||
starlink_headquarters_selfie
|
};
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeline: timelineEntry[] = [
|
const timeline: timelineEntry[] = [
|
||||||
{
|
{
|
||||||
event: "Started",
|
event: "Started",
|
||||||
eventDetail: "Satellite Hardware Test Team",
|
eventDetail: "Satellite Hardware Test Team",
|
||||||
date: "September 2019",
|
date: "September 2019",
|
||||||
description: "Owned test systems for four generations of Starlink flight computers and two generations of power boards"
|
description:
|
||||||
|
"Owned test systems for four generations of Starlink flight computers and two generations of power boards",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
event: "Transitioned To Remote",
|
event: "Transitioned To Remote",
|
||||||
eventDetail: "Moved To Oregon",
|
eventDetail: "Moved To Oregon",
|
||||||
date: "August 2022",
|
date: "August 2022",
|
||||||
description: "Personal decision, but I was allowed to work on tools for the build reliability engineering team"
|
description:
|
||||||
|
"Personal decision, but I was allowed to work on tools for the build reliability engineering team",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
event: "Changed Teams",
|
event: "Changed Teams",
|
||||||
eventDetail: "Components Test Infra Team",
|
eventDetail: "Components Test Infra Team",
|
||||||
date: "March 2024 - VERIFY",
|
date: "March 2024",
|
||||||
description: "Vertical move that allowed for broader application of my skills"
|
description:
|
||||||
|
"Vertical move that allowed for broader application of my skills",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
event: "Finished",
|
event: "Finished",
|
||||||
eventDetail: "Thanks for all the fish!",
|
eventDetail: "Thanks for all the fish!",
|
||||||
date: "April 2025",
|
date: "April 2025",
|
||||||
description: "Celebrated five and a half years of helping put thousands of satellites, and dozens of rockets, into orbit"
|
description:
|
||||||
}
|
"Celebrated five and a half years of helping put thousands of satellites, and dozens of rockets, into orbit",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<ExperienceLayout title="SpaceX - Hardware Test Engineer I/II">
|
<ExperienceLayout title="SpaceX - Hardware Test Engineer I/II">
|
||||||
<Carousel carouselGroup={headerCarouselGroup} />
|
<Carousel carouselGroup={headerCarouselGroup} />
|
||||||
|
<h2 class="my-4 font-bold md:text-2xl">Summary</h2>
|
||||||
<h2 class="font-bold md:text-2xl my-4">Summary</h2>
|
<h3 class="my-4 font-bold md:text-lg">Timeline</h3>
|
||||||
<h3 class="font-bold md:text-lg my-4">Timeline</h3>
|
|
||||||
<Timeline timeline={timeline} />
|
<Timeline timeline={timeline} />
|
||||||
<h3 class="font-bold md:text-lg my-4">Key Takeaways</h3>
|
<h3 class="my-4 font-bold md:text-lg">Key Takeaways</h3>
|
||||||
<ul class="list-disc list-inside">
|
<ul class="list-inside list-disc">
|
||||||
<li>Created test systems which validated ~4500 Starlink satellite flight computers, and ~4000 power boards</li>
|
<li>
|
||||||
<li>Developed program-critical infrastructure that enabled efficient triage, management, and tracking of hardware failures</li>
|
Created test systems which validated ~4500 Starlink satellite flight
|
||||||
<li>Designed and deployed automated, unified, and containerized infrastructure to greatly increase application reliability and development speed </li>
|
computers, and ~4000 power boards
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Developed program-critical infrastructure that enabled efficient triage,
|
||||||
|
management, and tracking of hardware failures
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Designed and deployed automated, unified, and containerized infrastructure
|
||||||
|
to greatly increase application reliability and development speed
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 class="font-bold md:text-lg my-4">Skills Used</h3>
|
<h3 class="my-4 font-bold md:text-lg">Skills Used</h3>
|
||||||
<div class="relative grid gap-6 grid-flow-row sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 border-caperren-green">
|
<div
|
||||||
|
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>
|
||||||
<div class="font-extrabold text-sm">Software</div>
|
<div class="text-sm font-extrabold">Software</div>
|
||||||
<hr class="text-caperren-green" />
|
<hr class="text-caperren-green" />
|
||||||
<ul class="list-disc list-inside text-sm">
|
<ul class="list-inside list-disc text-sm">
|
||||||
<li>Python</li>
|
<li>Python</li>
|
||||||
<li>Test Driven Development</li>
|
<li>Test Driven Development</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-extrabold text-sm">Electrical</div>
|
<div class="text-sm font-extrabold">Electrical</div>
|
||||||
<hr class="text-caperren-green" />
|
<hr class="text-caperren-green" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-extrabold text-sm">Mechanical</div>
|
<div class="text-sm font-extrabold">Mechanical</div>
|
||||||
<hr class="text-caperren-green" />
|
<hr class="text-caperren-green" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-extrabold text-sm">Other</div>
|
<div class="text-sm font-extrabold">Other</div>
|
||||||
<hr class="text-caperren-green" />
|
<hr class="text-caperren-green" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="font-bold md:text-2xl my-4">Details By Team</h2>
|
<h2 class="my-4 font-bold md:text-2xl">Details By Team</h2>
|
||||||
<h3 class="font-bold md:text-lg my-4">Starlink Hardware Test</h3>
|
<h3 class="my-4 font-bold md:text-lg">Starlink Hardware Test</h3>
|
||||||
<h3 class="font-bold md:text-lg my-4">Build Reliability Engineering</h3>
|
<h3 class="my-4 font-bold md:text-lg">Build Reliability Engineering</h3>
|
||||||
<h3 class="font-bold md:text-lg my-4">Components Test Infrastructure</h3>
|
<h3 class="my-4 font-bold md:text-lg">Components Test Infrastructure</h3>
|
||||||
|
|
||||||
</ExperienceLayout>
|
</ExperienceLayout>
|
||||||