Initialize Vue 3 portfolio site with Vite, Tailwind CSS, and GSAP. Add Docker support and GitHub Actions for deployment. Include essential project files, configurations, and a Product Requirements Document (PRD).
This commit is contained in:
commit
8458694866
47 files changed
+4155
No files matched your search
@@ -0,0 +1,12 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.cursor
|
||||||
|
PRD
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Optional: GitHub Pages project site base path (include slashes, e.g. /bio/)
|
||||||
|
# VITE_BASE=/bio/
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# GitHub Pages: set repository variable or secret VITE_BASE to /repo-name/ for project sites.
|
||||||
|
# User site (username.github.io) uses default "/" — omit VITE_BASE.
|
||||||
|
name: Deploy static site to GitHub Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
cache: "npm"
|
||||||
|
- run: npm install
|
||||||
|
- run: npm run build
|
||||||
|
env:
|
||||||
|
VITE_BASE: ${{ vars.VITE_BASE }}
|
||||||
|
- uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.local
|
||||||
|
.DS_Store
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/settings.json
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"],
|
||||||
|
"unwantedRecommendations": ["octref.vetur"]
|
||||||
|
}
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||||
|
"[vue]": {
|
||||||
|
"editor.defaultFormatter": "Vue.volar"
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
# --- build ---
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Default `/` is correct for serving at container root. Override only if you reverse-proxy under a subpath.
|
||||||
|
ARG VITE_BASE=/
|
||||||
|
ENV VITE_BASE=${VITE_BASE}
|
||||||
|
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# --- run ---
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget -qO- http://127.0.0.1/ > /dev/null || exit 1
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
This **Product Requirements Document (PRD)** outlines a professional, visually dynamic portfolio site designed to showcase a high-level blend of technical leadership and hands-on engineering.
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
# PRD: Professional Portfolio & Technical Showcase
|
||||||
|
|
||||||
|
## 1\. Project Overview
|
||||||
|
|
||||||
|
A centralized, modern web presence that serves as a professional hub. The site must balance the authority of a **Senior Solutions Architect** with the creative technicality of a **Full-Stack Developer**, all while maintaining an aesthetic rooted in the North County San Diego coastline.
|
||||||
|
|
||||||
|
## 2\. Visual Identity & Brand
|
||||||
|
|
||||||
|
- **Color Palette:** "SoCal Seaside"
|
||||||
|
|
||||||
|
- **Primary:** Deep Ocean Blues and Slate (for text and structural elements).
|
||||||
|
|
||||||
|
- **Secondary:** Coastal Teals and Sky Blues (for accents and hover states).
|
||||||
|
|
||||||
|
- **Highlights:** Warm Gold and Soft Sand (for CTAs, borders, or highlights—mimicking a sunset over the Pacific).
|
||||||
|
|
||||||
|
- **Design Language:** Clean, minimalist, and high-contrast. Use glassmorphism (frosted glass) effects for cards to keep the feeling "light" and "airy."
|
||||||
|
|
||||||
|
- **Dynamic Elements:** \* Subtle animated background (e.g., gentle CSS waves or a particle system representing "data flow").
|
||||||
|
|
||||||
|
- Interactive "Skill Clouds" or a dynamic terminal-style window.
|
||||||
|
|
||||||
|
## 3\. Core Features & Functional Requirements
|
||||||
|
|
||||||
|
### 3.1. Hero Section
|
||||||
|
|
||||||
|
- **Objective:** Immediate impact and role identification.
|
||||||
|
|
||||||
|
- **Content:** Name, high-level title (Software Developer | Solutions Architect), and a punchy value proposition.
|
||||||
|
|
||||||
|
- **Visual:** A professional headshot or a high-end stylized graphic, paired with a "Quick Links" bar (LinkedIn, GitHub, Email icons).
|
||||||
|
|
||||||
|
|
||||||
|
### 3.2. Technical Skill Matrix
|
||||||
|
|
||||||
|
- **Objective:** Display a broad stack at a glance.
|
||||||
|
|
||||||
|
- **Categories:** \* **Core Tech:** (e.g., Vue.js, Python, Node.js).
|
||||||
|
|
||||||
|
- **Infrastructure:** (e.g., Proxmox, Docker, WireGuard).
|
||||||
|
|
||||||
|
- **AI/LLM:** (e.g., Gemini API, Ollama, RAG development).
|
||||||
|
|
||||||
|
- **Sales Engineering:** (e.g., Technical Demos, PRD creation, Solution Architecture).
|
||||||
|
|
||||||
|
|
||||||
|
### 3.3. Project & Demo Gallery (The "Home Lab" Showcase)
|
||||||
|
|
||||||
|
- **Objective:** Prove technical depth through live examples.
|
||||||
|
|
||||||
|
- **Structure:** Grid layout of "Project Cards."
|
||||||
|
|
||||||
|
- **Specific Items:**
|
||||||
|
|
||||||
|
- **LoreKeeper:** Story-writing assistant (Highlighting Vue/Python/AI).
|
||||||
|
|
||||||
|
- **Home Assistant Dashboard:** Custom Vue 3 dashboard.
|
||||||
|
|
||||||
|
- **Test Demo Sites:** Links to external subdomains or containers hosting specific proof-of-concepts.
|
||||||
|
|
||||||
|
|
||||||
|
### 3.4. Experience & Education (Digital Resume)
|
||||||
|
|
||||||
|
- **Objective:** Provide the "paper" trail.
|
||||||
|
|
||||||
|
- **Feature:** An interactive timeline or accordion-style list of roles (notably LambdaTest and previous senior SE positions).
|
||||||
|
|
||||||
|
- **Download:** A prominent "Download PDF Resume" button.
|
||||||
|
|
||||||
|
|
||||||
|
### 3.5. Interests (The Human Element)
|
||||||
|
|
||||||
|
- **Objective:** Culture fit and personality.
|
||||||
|
|
||||||
|
- **Content:** Brief highlights of gardening (Meyer lemons), long-distance running, and passion for storytelling/One Piece.
|
||||||
|
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## 4\. Technical Stack Recommendations
|
||||||
|
|
||||||
|
- **Frontend:** **Vue 3 (Vite)** for speed and familiarity.
|
||||||
|
|
||||||
|
- **Styling:** **Tailwind CSS** (perfect for rapid implementation of the SoCal color profile).
|
||||||
|
|
||||||
|
- **Animations:** **Framer Motion** (for Vue) or **GSAP** for high-end scroll interactions.
|
||||||
|
|
||||||
|
- **Hosting:** Hosted on your home lab (via **Cloudflare Tunnel** or **WireGuard**) to demonstrate infrastructure skills, or **Vercel** for 99.9% uptime.
|
||||||
|
|
||||||
|
|
||||||
|
## 5\. Information Architecture (Site Map)
|
||||||
|
|
||||||
|
1. **Home (Single Page Scroll):**
|
||||||
|
|
||||||
|
- Hero -> Skills -> Work/Projects -> Experience -> Interests -> Contact.
|
||||||
|
2. **Navigation:**
|
||||||
|
|
||||||
|
- Sticky header with "Jump to" links.
|
||||||
|
|
||||||
|
- Dark/Light mode toggle (styled as Sun/Moon).
|
||||||
|
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
## 6\. Success Metrics
|
||||||
|
|
||||||
|
- **Performance:** Lighthouse score of 90+ for Performance and SEO.
|
||||||
|
|
||||||
|
- **Responsiveness:** Perfect rendering on mobile (for recruiters on the go) and ultrawide monitors (for engineers).
|
||||||
|
|
||||||
|
- **Engagement:** Successful click-throughs to the GitHub and live Demo sites.
|
||||||
|
|
||||||
|
|
||||||
|
Would you like to dive deeper into the specific Vue components for that dynamic Home Assistant dashboard link, or perhaps refine the wording for the professional "About" section?
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Bio portfolio (static)
|
||||||
|
|
||||||
|
Single-page portfolio aligned with [PRD/PRDs/Bio Site PRD.md](PRD/PRDs/Bio%20Site%20PRD.md): Vue 3, Vite, Tailwind, GSAP scroll reveals, static `dist/` output for free static hosts.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js 20+ (22 recommended for CI)
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open the URL Vite prints (usually `http://localhost:5173`).
|
||||||
|
|
||||||
|
## Production build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Artifacts are written to `dist/`. Deploy that folder to **Cloudflare Pages**, **Netlify**, **Vercel**, or **GitHub Pages**.
|
||||||
|
|
||||||
|
## Docker (static site in a container)
|
||||||
|
|
||||||
|
The app is still a **static** build; the container only runs **nginx** to serve `dist/`. No Node at runtime.
|
||||||
|
|
||||||
|
### Build and run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t bio-site .
|
||||||
|
docker run --rm -p 8080:80 bio-site
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:8080`.
|
||||||
|
|
||||||
|
### Compose (handy defaults)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
By default this maps **host `8080` → container `80`**. Override the host port:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HOST_PORT=3000 docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### `VITE_BASE` inside Docker
|
||||||
|
|
||||||
|
- For **http://your-server/`** (site at root): use the default **`/`** (do not set `VITE_BASE`, or set `VITE_BASE=/`).
|
||||||
|
- For **GitHub Pages–style subpaths** behind a path-based reverse proxy only: build with e.g. `docker build --build-arg VITE_BASE=/bio/ -t bio-site .` or set `VITE_BASE` in `docker-compose.yml` / `.env` next to compose.
|
||||||
|
|
||||||
|
Mismatching `VITE_BASE` and where nginx actually serves the app is the most common reason assets return 404.
|
||||||
|
|
||||||
|
### Hosting on your LAN (Linux server)
|
||||||
|
|
||||||
|
1. **Install Docker** on the server (Docker Engine + Compose plugin, or Docker Desktop on a workstation acting as a host).
|
||||||
|
2. **Copy the project** (or clone the repo) onto the machine and run `docker compose up -d --build` from the repo root.
|
||||||
|
3. **Find the server’s LAN IP**, e.g. `ip -br a` or `hostname -I`.
|
||||||
|
4. From another device on the **same network**, open `http://<LAN-IP>:8080` (or whatever `HOST_PORT` you chose).
|
||||||
|
5. **Firewall**: allow the host port, e.g. with UFW: `sudo ufw allow 8080/tcp comment 'bio static'` then `sudo ufw reload` (adjust port to match `HOST_PORT`).
|
||||||
|
6. **Optional**: put **Caddy**, **Traefik**, or **nginx** on the host in front of the container for HTTPS, auth, or a single entrypoint on 443 while the container stays on an internal port.
|
||||||
|
|
||||||
|
The container listens on **all interfaces** inside the host’s Docker publish rules; binding to LAN-only is usually done with host firewall rules or by placing a reverse proxy on the edge.
|
||||||
|
|
||||||
|
## Customize content
|
||||||
|
|
||||||
|
| What | Where |
|
||||||
|
|------|--------|
|
||||||
|
| Name, links, email, tagline | `src/content/site.ts` |
|
||||||
|
| Skills | `src/content/skills.ts` |
|
||||||
|
| Projects | `src/content/projects.ts` |
|
||||||
|
| Roles | `src/content/experience.ts` |
|
||||||
|
| Headshot | Replace `public/avatar.svg` with your image (WebP/AVIF or optimized JPG/PNG under `public/` — update `HeroSection.vue` if you change the filename). |
|
||||||
|
| Resume PDF | Add `public/resume.pdf` (the Experience section links to it). |
|
||||||
|
| SEO / social | Update `<title>`, `<meta name="description">`, Open Graph/Twitter tags, and `og:url` in `index.html` to match your real domain. |
|
||||||
|
|
||||||
|
## GitHub Pages (project site)
|
||||||
|
|
||||||
|
If the site is served from `https://username.github.io/repo-name/`, Vite needs a **base path**:
|
||||||
|
|
||||||
|
1. In the repo, add a variable **`VITE_BASE`** (Settings → Secrets and variables → Actions → Variables) with value `/repo-name/` (leading slash, trailing slash optional).
|
||||||
|
2. Enable GitHub Pages from **GitHub Actions** using the included workflow (Settings → Pages → Build and deployment → Source: GitHub Actions).
|
||||||
|
3. The workflow [`.github/workflows/pages.yml`](.github/workflows/pages.yml) runs `npm install`, `npm run build` with `VITE_BASE`, and publishes `dist`.
|
||||||
|
|
||||||
|
For a **user** site (`username.github.io` with the site at the root of that repo), leave `VITE_BASE` unset so the site uses `/`.
|
||||||
|
|
||||||
|
## Environment variable (local)
|
||||||
|
|
||||||
|
Create `.env.production` (or `.env`) if you want to test a non-root base locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VITE_BASE=/repo-name/
|
||||||
|
```
|
||||||
|
|
||||||
|
Then `npm run build` and `npm run preview`.
|
||||||
|
|
||||||
|
## Lighthouse / performance notes
|
||||||
|
|
||||||
|
- Hero image uses `fetchpriority="high"` and explicit `width` / `height`.
|
||||||
|
- GSAP + ScrollTrigger load **on demand** per section via dynamic `import()` to keep the initial JS smaller.
|
||||||
|
- Google Fonts load from `index.html`; self-hosting fonts is an optional future optimization.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Private / personal use — adjust as you like.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
# Leave default `/` when the site is at the root of the hostname (typical home LAN).
|
||||||
|
# Set e.g. `/bio/` only if you serve behind a path-prefix reverse proxy.
|
||||||
|
VITE_BASE: ${VITE_BASE:-/}
|
||||||
|
ports:
|
||||||
|
- "${HOST_PORT:-8080}:80"
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# SPA / static: serve files if present, else index.html
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache hashed assets from Vite
|
||||||
|
location /assets/ {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/css application/javascript application/json image/svg+xml;
|
||||||
|
gzip_min_length 256;
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Software Developer and Solutions Architect — technical leadership, full-stack engineering, and solution architecture."
|
||||||
|
/>
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:title" content="Portfolio — Developer & Solutions Architect" />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="Software Developer and Solutions Architect — technical leadership, full-stack engineering, and solution architecture."
|
||||||
|
/>
|
||||||
|
<meta property="og:url" content="https://example.com" />
|
||||||
|
<meta property="og:image" content="/og-image.svg" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="Portfolio — Developer & Solutions Architect" />
|
||||||
|
<meta
|
||||||
|
name="twitter:description"
|
||||||
|
content="Software Developer and Solutions Architect — technical leadership, full-stack engineering, and solution architecture."
|
||||||
|
/>
|
||||||
|
<meta name="twitter:image" content="/og-image.svg" />
|
||||||
|
<title>Portfolio — Developer & Solutions Architect</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&family=Outfit:wght@500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+2436
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "bio-site",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"gsap": "^3.12.7",
|
||||||
|
"vue": "^3.5.13"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"tailwindcss": "^3.4.16",
|
||||||
|
"typescript": "~5.6.3",
|
||||||
|
"vite": "^6.0.3",
|
||||||
|
"vue-tsc": "^2.1.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="sky" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#38bdf8"/>
|
||||||
|
<stop offset="55%" stop-color="#0d9488"/>
|
||||||
|
<stop offset="100%" stop-color="#0f2137"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="sand" x1="0" y1="1" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stop-color="#f4e4c1"/>
|
||||||
|
<stop offset="100%" stop-color="#d4a574"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="480" height="480" fill="url(#sky)"/>
|
||||||
|
<path
|
||||||
|
fill="url(#sand)"
|
||||||
|
opacity="0.35"
|
||||||
|
d="M0 360c80-40 160-20 240 0s160 40 240 20v120H0z"
|
||||||
|
/>
|
||||||
|
<circle cx="240" cy="175" r="72" fill="#f4e4c1" opacity="0.9"/>
|
||||||
|
<path
|
||||||
|
fill="#f4e4c1"
|
||||||
|
opacity="0.85"
|
||||||
|
d="M120 400c40-100 200-100 240 0v80H120z"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x="240"
|
||||||
|
y="430"
|
||||||
|
text-anchor="middle"
|
||||||
|
fill="#0a1628"
|
||||||
|
font-family="system-ui,sans-serif"
|
||||||
|
font-size="18"
|
||||||
|
opacity="0.55"
|
||||||
|
>
|
||||||
|
Replace with photo
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||||
|
<g>
|
||||||
|
<path d="M 0.00 0.00 L 767.93 0.00 C524.54,0.00 511.89,0.09 512.36,1.75 C512.63,2.71 513.14,7.78 513.49,13.00 C514.63,29.98 520.51,69.81 524.71,89.00 C526.09,95.32 528.04,104.34 529.03,109.04 C531.34,119.91 538.66,149.10 540.56,155.00 C541.35,157.48 543.39,164.45 545.08,170.50 C548.13,181.39 559.20,215.64 560.91,219.50 C561.40,220.60 562.82,224.43 564.05,228.00 C567.85,238.99 574.83,256.77 581.57,272.63 C585.11,280.95 588.00,288.00 588.00,288.28 C588.00,288.79 592.65,298.98 599.74,314.00 C601.69,318.12 605.27,325.77 607.71,331.00 C612.72,341.76 619.89,356.00 621.05,357.50 C621.48,358.05 623.72,362.33 626.03,367.00 C629.86,374.73 637.03,387.64 644.28,399.83 C645.81,402.40 647.51,405.40 648.05,406.50 C653.35,417.12 679.53,458.58 695.74,482.00 C715.19,510.11 730.93,531.56 741.78,544.73 C745.92,549.77 748.10,554.03 746.74,554.46 C631.93,590.36 537.63,639.47 447.16,710.49 C440.98,715.33 440.77,715.41 439.31,713.48 C436.26,709.46 420.95,676.79 414.67,660.89 C393.18,606.54 382.99,553.21 383.00,495.11 C383.01,462.19 385.81,433.34 392.09,401.50 C395.11,386.24 401.24,361.33 402.99,357.25 C403.62,355.78 403.61,355.00 402.96,355.00 C399.72,355.00 358.17,377.86 334.00,392.95 C237.17,453.39 156.27,532.73 96.87,625.50 C60.27,682.66 29.80,746.57 7.89,812.12 L 0.00 835.73 Z" fill="rgb(224,242,254)"/>
|
||||||
|
<path d="M 1024.00 1024.00 L 0.00 1024.00 L 803.50 1023.91 L 792.50 1018.78 C731.41,990.28 670.24,954.04 621.01,917.17 C540.57,856.92 479.36,788.10 439.02,713.07 C439.13,713.23 439.22,713.37 439.31,713.48 C440.77,715.41 440.98,715.33 447.16,710.49 C537.63,639.47 631.93,590.36 746.74,554.46 C747.44,554.24 747.20,553.00 746.24,551.17 C747.68,553.23 748.78,553.99 749.82,553.65 C754.22,552.21 790.09,542.63 802.82,539.49 C833.40,531.95 881.76,523.14 915.00,519.04 C943.85,515.49 992.74,512.00 1013.75,512.00 L 1024.00 512.00 ZM 966.57 986.43 C967.44,987.29 968.25,988.00 968.38,988.00 C968.51,988.00 970.30,985.40 972.36,982.22 C975.85,976.83 983.90,970.00 986.77,970.00 C989.12,970.00 988.08,966.24 985.55,965.60 C981.80,964.66 971.25,954.40 970.49,950.96 C969.65,947.13 966.49,946.92 965.56,950.63 C964.61,954.38 954.41,964.75 950.90,965.52 C948.98,965.94 948.00,966.78 948.00,968.00 C948.00,969.22 948.98,970.06 950.90,970.48 C953.75,971.10 965.00,981.78 965.00,983.85 C965.00,984.41 965.71,985.56 966.57,986.43 ZM 392.63 588.58 C392.11,586.23 391.62,583.87 391.14,581.50 C387.80,565.02 385.49,547.35 384.21,529.11 C385.66,549.31 388.45,569.05 392.63,588.58 ZM 395.71 384.82 C398.57,371.98 401.40,360.96 402.99,357.25 C401.80,360.02 398.59,372.39 395.71,384.82 ZM 430.52 696.54 C426.11,687.55 421.99,678.48 418.16,669.34 C421.80,677.81 426.45,688.03 430.52,696.54 ZM 386.15 438.96 C386.58,435.24 387.04,431.54 387.56,427.87 C387.96,424.92 388.49,421.57 389.11,417.97 C387.96,425.03 386.97,432.00 386.15,438.96 ZM 741.56 544.47 C741.38,544.25 741.20,544.04 741.02,543.82 C741.43,544.31 741.49,544.39 741.56,544.47 Z" fill="rgb(210,176,138)"/>
|
||||||
|
<path d="M 0.00 835.73 L 7.89 812.12 C29.80,746.57 60.27,682.66 96.87,625.50 C156.27,532.73 237.17,453.39 334.00,392.95 C358.17,377.86 399.72,355.00 402.96,355.00 C403.61,355.00 403.62,355.78 402.99,357.25 C401.40,360.96 398.57,371.98 395.71,384.82 C394.35,390.70 393.06,396.60 392.09,401.50 C390.99,407.07 390.00,412.54 389.11,417.97 C388.49,421.57 387.96,424.92 387.56,427.87 C387.04,431.54 386.58,435.24 386.15,438.96 C384.01,457.23 383.01,475.45 383.00,495.11 C383.00,506.62 383.40,517.93 384.21,529.11 C385.49,547.35 387.80,565.02 391.14,581.50 C391.62,583.87 392.11,586.23 392.63,588.58 C397.81,612.82 405.12,636.76 414.67,660.89 C415.64,663.36 416.83,666.23 418.16,669.34 C418.17,669.36 418.18,669.38 418.19,669.40 C422.01,678.53 426.12,687.57 430.52,696.54 C434.29,704.44 437.56,710.87 439.02,713.07 C479.36,788.10 540.57,856.92 621.01,917.17 C670.24,954.04 731.41,990.28 792.50,1018.78 L 803.50 1023.91 L 0.00 1024.00 L 0.00 835.73 ZM 966.57 986.43 C965.71,985.56 965.00,984.41 965.00,983.85 C965.00,981.78 953.75,971.10 950.90,970.48 C948.98,970.06 948.00,969.22 948.00,968.00 C948.00,966.78 948.98,965.94 950.90,965.52 C954.41,964.75 964.61,954.38 965.56,950.63 C966.49,946.92 969.65,947.13 970.49,950.96 C971.25,954.40 981.80,964.66 985.55,965.60 C988.08,966.24 989.12,970.00 986.77,970.00 C983.90,970.00 975.85,976.83 972.36,982.22 C970.30,985.40 968.51,988.00 968.38,988.00 C968.25,988.00 967.44,987.29 966.57,986.43 Z" fill="rgb(7,89,133)"/>
|
||||||
|
<path d="M 745.69 550.35 C744.34,548.23 742.40,545.48 741.37,544.24 C731.13,531.82 714.89,509.68 695.74,482.00 C679.53,458.58 653.35,417.12 648.05,406.50 C647.51,405.40 645.81,402.40 644.28,399.83 C637.03,387.64 629.86,374.73 626.03,367.00 C623.72,362.33 621.48,358.05 621.05,357.50 C619.89,356.00 612.72,341.76 607.71,331.00 C605.27,325.77 601.69,318.12 599.74,314.00 C592.65,298.98 588.00,288.79 588.00,288.28 C588.00,288.00 585.11,280.95 581.57,272.63 C574.83,256.77 567.85,238.99 564.05,228.00 C562.82,224.43 561.40,220.60 560.91,219.50 C559.20,215.64 548.13,181.39 545.08,170.50 C543.39,164.45 541.35,157.48 540.56,155.00 C538.66,149.10 531.34,119.91 529.03,109.04 C528.04,104.34 526.09,95.32 524.71,89.00 C520.51,69.81 514.63,29.98 513.49,13.00 C513.14,7.78 512.63,2.71 512.36,1.75 C511.89,0.09 524.54,0.00 767.93,0.00 L 1024.00 0.00 L 1024.00 256.00 L 1024.00 512.00 L 1013.75 512.00 C992.74,512.00 943.85,515.49 915.00,519.04 C881.76,523.14 833.40,531.95 802.82,539.49 C790.09,542.63 754.22,552.21 749.82,553.65 C748.65,554.03 747.41,553.03 745.69,550.35 Z" fill="rgb(252,132,40)"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#0a1628"/>
|
||||||
|
<stop offset="100%" stop-color="#152a45"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="1200" height="630" fill="url(#g)"/>
|
||||||
|
<text x="80" y="200" fill="#e8d5b7" font-family="system-ui,sans-serif" font-size="28" opacity="0.85">Portfolio</text>
|
||||||
|
<text x="80" y="310" fill="#f4f4f5" font-family="system-ui,sans-serif" font-size="56" font-weight="700">Developer & Solutions Architect</text>
|
||||||
|
<text x="80" y="390" fill="#7dd3fc" font-family="system-ui,sans-serif" font-size="26" opacity="0.9">Vue � TypeScript � Infrastructure � AI</text>
|
||||||
|
<circle cx="1020" cy="200" r="120" fill="#2dd4bf" opacity="0.15"/>
|
||||||
|
<circle cx="980" cy="480" r="160" fill="#d4a574" opacity="0.12"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 901 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<rect width="32" height="32" rx="8" fill="#0f2137"/>
|
||||||
|
<path d="M8 22V10l4.5 6L17 10v12" stroke="#2dd4bf" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M20 10h6M23 10v12" stroke="#d4a574" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 343 B |
Binary file not shown.
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import SiteHeader from "./components/SiteHeader.vue";
|
||||||
|
import WaveBackground from "./components/WaveBackground.vue";
|
||||||
|
import HeroSection from "./components/HeroSection.vue";
|
||||||
|
import FakeTerminal from "./components/FakeTerminal.vue";
|
||||||
|
import SkillMatrix from "./components/SkillMatrix.vue";
|
||||||
|
import ProjectGrid from "./components/ProjectGrid.vue";
|
||||||
|
import ExperienceSection from "./components/ExperienceSection.vue";
|
||||||
|
import InterestsSection from "./components/InterestsSection.vue";
|
||||||
|
import ContactFooter from "./components/ContactFooter.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a
|
||||||
|
href="#main-content"
|
||||||
|
class="focus-ring fixed left-4 top-4 z-[100] -translate-y-24 rounded-lg bg-white px-4 py-2 text-sm font-semibold text-ocean-900 shadow-lg transition focus:translate-y-0 dark:bg-ocean-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Skip to main content
|
||||||
|
</a>
|
||||||
|
<WaveBackground />
|
||||||
|
<SiteHeader />
|
||||||
|
<main id="main-content">
|
||||||
|
<HeroSection />
|
||||||
|
<FakeTerminal />
|
||||||
|
<SkillMatrix />
|
||||||
|
<ProjectGrid />
|
||||||
|
<ExperienceSection />
|
||||||
|
<InterestsSection />
|
||||||
|
<ContactFooter />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { site } from "../content/site";
|
||||||
|
import { useSectionReveal } from "../composables/useSectionReveal";
|
||||||
|
|
||||||
|
const root = ref<HTMLElement | null>(null);
|
||||||
|
useSectionReveal(root);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<footer
|
||||||
|
id="contact"
|
||||||
|
ref="root"
|
||||||
|
class="border-t border-ocean-800/10 bg-white/40 py-16 dark:border-white/5 dark:bg-ocean-950/40 sm:py-20"
|
||||||
|
aria-labelledby="contact-heading"
|
||||||
|
>
|
||||||
|
<div class="section-inner text-center">
|
||||||
|
<h2
|
||||||
|
id="contact-heading"
|
||||||
|
class="font-display text-3xl font-bold tracking-tight text-ocean-900 dark:text-white sm:text-4xl"
|
||||||
|
>
|
||||||
|
Let’s connect
|
||||||
|
</h2>
|
||||||
|
<p class="mx-auto mt-3 max-w-xl text-slate-mist dark:text-coast-foam/75">
|
||||||
|
Best for architecture conversations, demo collaborations, or a thoughtful thread on distributed systems
|
||||||
|
(and/or shonen pacing).
|
||||||
|
</p>
|
||||||
|
<div class="mt-8 flex flex-wrap justify-center gap-3">
|
||||||
|
<a
|
||||||
|
:href="`mailto:${site.email}`"
|
||||||
|
class="focus-ring glass-card inline-flex items-center gap-2 px-5 py-3 text-sm font-semibold text-ocean-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Email {{ site.email }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="site.linkedinUrl"
|
||||||
|
class="focus-ring glass-card inline-flex items-center gap-2 px-5 py-3 text-sm font-semibold text-ocean-900 dark:text-white"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
LinkedIn
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="site.githubUrl"
|
||||||
|
class="focus-ring glass-card inline-flex items-center gap-2 px-5 py-3 text-sm font-semibold text-ocean-900 dark:text-white"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="mt-10 text-xs text-slate-mist dark:text-coast-foam/50">
|
||||||
|
Built with Vue 3, Vite, and Tailwind — static export, no server required.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { roles } from "../content/experience";
|
||||||
|
import { useSectionReveal } from "../composables/useSectionReveal";
|
||||||
|
|
||||||
|
const root = ref<HTMLElement | null>(null);
|
||||||
|
useSectionReveal(root);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
id="experience"
|
||||||
|
ref="root"
|
||||||
|
class="section-inner scroll-mt-24 py-16 sm:py-20"
|
||||||
|
aria-labelledby="experience-heading"
|
||||||
|
>
|
||||||
|
<div class="mx-auto max-w-2xl text-center">
|
||||||
|
<h2
|
||||||
|
id="experience-heading"
|
||||||
|
class="font-display text-3xl font-bold tracking-tight text-ocean-900 dark:text-white sm:text-4xl"
|
||||||
|
>
|
||||||
|
Experience
|
||||||
|
</h2>
|
||||||
|
<p class="mt-3 text-slate-mist dark:text-coast-foam/75">
|
||||||
|
Replace placeholders in <code class="font-mono text-sm">src/content/experience.ts</code> with your real roles.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="relative mx-auto mt-14 max-w-3xl pl-8 sm:pl-10">
|
||||||
|
<div
|
||||||
|
class="absolute left-2 top-2 bottom-2 w-px bg-gradient-to-b from-coast-teal via-coast-sky/50 to-sunset-gold/40 sm:left-3"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<ol class="space-y-10">
|
||||||
|
<li v-for="r in roles" :key="r.id" class="relative">
|
||||||
|
<span
|
||||||
|
class="absolute -left-[1.35rem] top-1 flex h-4 w-4 items-center justify-center rounded-full border-2 border-coast-teal bg-sand-warm dark:border-sunset-gold dark:bg-ocean-900 sm:-left-[1.6rem]"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<span class="h-1.5 w-1.5 rounded-full bg-coast-teal dark:bg-sunset-gold" />
|
||||||
|
</span>
|
||||||
|
<article class="glass-card p-5 sm:p-6">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-coast-deep dark:text-coast-teal">
|
||||||
|
{{ r.period }}
|
||||||
|
</p>
|
||||||
|
<h3 class="mt-1 font-display text-xl font-semibold text-ocean-900 dark:text-white">
|
||||||
|
{{ r.title }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm font-medium text-slate-mist dark:text-coast-foam/80">
|
||||||
|
{{ r.company }}
|
||||||
|
</p>
|
||||||
|
<ul class="mt-4 space-y-2 text-sm leading-relaxed text-slate-mist dark:text-coast-foam/85">
|
||||||
|
<li v-for="(h, hi) in r.highlights" :key="hi" class="flex gap-2">
|
||||||
|
<span class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-sunset-gold/80" aria-hidden="true" />
|
||||||
|
<span>{{ h }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div class="mt-12 flex justify-center">
|
||||||
|
<a
|
||||||
|
href="/resume.pdf"
|
||||||
|
class="focus-ring glass-card inline-flex items-center gap-2 px-6 py-3 text-sm font-semibold text-ocean-900 dark:text-white"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5 text-sunset-gold" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v12m0 0l4-4m-4 4L8 11M5 21h14" />
|
||||||
|
</svg>
|
||||||
|
Download PDF resume
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted, ref } from "vue";
|
||||||
|
import { useSectionReveal } from "../composables/useSectionReveal";
|
||||||
|
|
||||||
|
const root = ref<HTMLElement | null>(null);
|
||||||
|
useSectionReveal(root);
|
||||||
|
|
||||||
|
const lines = ref<string[]>([]);
|
||||||
|
const fullScript = [
|
||||||
|
"$ ssh portfolio.lab",
|
||||||
|
"Welcome to matt@bio — GNU/Linux",
|
||||||
|
"$ cat ./values.txt",
|
||||||
|
"- System Integrity: Architecture must be secure by design and resilient at scale.",
|
||||||
|
"- Technical Clarity: The best solution is the one that stakeholders actually understand.",
|
||||||
|
"- Evidence-Based Trust: Demo the value, document the proof, then deploy the vision.",
|
||||||
|
"- Global Perspective: Code and products should transcend borders and languages.",
|
||||||
|
"- Continuous Lab-Culture: Innovation starts with curiosity and local experimentation.",
|
||||||
|
"$ ./stack --highlight",
|
||||||
|
"vue.js python docker local-llms sales-engineering",
|
||||||
|
"$ _",
|
||||||
|
];
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
let timer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
timer = setInterval(() => {
|
||||||
|
if (idx >= fullScript.length) {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lines.value = [...lines.value, fullScript[idx]!];
|
||||||
|
idx += 1;
|
||||||
|
}, 520);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section ref="root" class="section-inner pb-12" aria-label="Terminal style introduction">
|
||||||
|
<div
|
||||||
|
class="glass-card overflow-hidden font-mono text-sm shadow-lg dark:bg-ocean-900/70"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 border-b border-ocean-800/10 bg-ocean-950/5 px-4 py-2 dark:border-white/10 dark:bg-black/20"
|
||||||
|
>
|
||||||
|
<span class="h-2.5 w-2.5 rounded-full bg-red-400/90" />
|
||||||
|
<span class="h-2.5 w-2.5 rounded-full bg-amber-300/90" />
|
||||||
|
<span class="h-2.5 w-2.5 rounded-full bg-coast-teal/90" />
|
||||||
|
<span class="ml-2 text-xs text-slate-mist">portfolio — zsh</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5 px-4 py-4 text-ocean-900 dark:text-coast-foam/90">
|
||||||
|
<p v-for="(line, i) in lines" :key="i" class="whitespace-pre-wrap leading-relaxed">
|
||||||
|
<span v-if="line.startsWith('$')" class="text-coast-deep dark:text-coast-teal">{{ line.slice(0, 2) }}</span
|
||||||
|
><span v-if="line.startsWith('$')">{{ line.slice(2) }}</span>
|
||||||
|
<span v-else class="text-slate-mist dark:text-coast-foam/75">{{ line }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center gap-1 text-coast-deep/70 dark:text-coast-teal/80" aria-hidden="true">
|
||||||
|
<span class="inline-block h-4 w-2 animate-pulse bg-current" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { site } from "../content/site";
|
||||||
|
import { useSectionReveal } from "../composables/useSectionReveal";
|
||||||
|
|
||||||
|
const root = ref<HTMLElement | null>(null);
|
||||||
|
useSectionReveal(root);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
id="hero"
|
||||||
|
ref="root"
|
||||||
|
class="section-inner scroll-mt-24 pb-16 pt-12 sm:pb-24 sm:pt-16 lg:pt-20"
|
||||||
|
aria-labelledby="hero-heading"
|
||||||
|
>
|
||||||
|
<div class="grid items-center gap-10 lg:grid-cols-2 lg:gap-14">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<p
|
||||||
|
class="inline-flex items-center gap-2 rounded-full border border-coast-deep/20 bg-white/50 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-coast-deep dark:border-coast-teal/30 dark:bg-ocean-900/40 dark:text-coast-teal"
|
||||||
|
>
|
||||||
|
Encinitas, San Diego County, California
|
||||||
|
</p>
|
||||||
|
<h1
|
||||||
|
id="hero-heading"
|
||||||
|
class="font-display text-4xl font-bold tracking-tight text-ocean-900 dark:text-white sm:text-5xl lg:text-6xl"
|
||||||
|
>
|
||||||
|
{{ site.name }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg font-medium text-coast-deep dark:text-coast-teal sm:text-xl">
|
||||||
|
{{ site.titleLine }}
|
||||||
|
</p>
|
||||||
|
<p class="max-w-xl text-base leading-relaxed text-slate-mist dark:text-coast-foam/80 sm:text-lg">
|
||||||
|
{{ site.tagline }}
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-wrap items-center gap-3 pt-2">
|
||||||
|
<a
|
||||||
|
:href="site.linkedinUrl"
|
||||||
|
class="focus-ring glass-card inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-ocean-900 transition hover:border-coast-deep/50 dark:text-white"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
LinkedIn
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="site.githubUrl"
|
||||||
|
class="focus-ring glass-card inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-ocean-900 transition hover:border-coast-deep/50 dark:text-white"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="`mailto:${site.email}`"
|
||||||
|
class="focus-ring glass-card inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-ocean-900 transition hover:border-sunset-gold/60 dark:text-white"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16v12H4z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 7l8 6 8-6" />
|
||||||
|
</svg>
|
||||||
|
Email
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative mx-auto w-full max-w-md lg:max-w-none">
|
||||||
|
<div
|
||||||
|
class="glass-card relative overflow-hidden p-2 shadow-lg animate-float"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/me.jpg"
|
||||||
|
alt="Matthew Edwards"
|
||||||
|
width="480"
|
||||||
|
height="480"
|
||||||
|
class="aspect-square w-full rounded-xl object-cover"
|
||||||
|
decoding="async"
|
||||||
|
fetchpriority="high"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute inset-0 rounded-xl ring-1 ring-inset ring-sunset-gold/25"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useSectionReveal } from "../composables/useSectionReveal";
|
||||||
|
|
||||||
|
const root = ref<HTMLElement | null>(null);
|
||||||
|
useSectionReveal(root);
|
||||||
|
|
||||||
|
const interests = [
|
||||||
|
{
|
||||||
|
title: "Gardening",
|
||||||
|
body: "Meyer lemons on the patio — patience, seasonal rhythm, and the occasional battle with snails.",
|
||||||
|
emoji: "🍋",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Long-distance running",
|
||||||
|
body: "Miles for clear thinking. Training plans double as a reminder that compounding beats heroics.",
|
||||||
|
emoji: "🏃",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Storytelling",
|
||||||
|
body: "Narrative structure shows up in demos and docs alike. Current serialized obsession: One Piece.",
|
||||||
|
emoji: "📖",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
id="interests"
|
||||||
|
ref="root"
|
||||||
|
class="section-inner scroll-mt-24 py-16 sm:py-20"
|
||||||
|
aria-labelledby="interests-heading"
|
||||||
|
>
|
||||||
|
<div class="mx-auto max-w-2xl text-center">
|
||||||
|
<h2
|
||||||
|
id="interests-heading"
|
||||||
|
class="font-display text-3xl font-bold tracking-tight text-ocean-900 dark:text-white sm:text-4xl"
|
||||||
|
>
|
||||||
|
Beyond the keyboard
|
||||||
|
</h2>
|
||||||
|
<p class="mt-3 text-slate-mist dark:text-coast-foam/75">
|
||||||
|
The human side — how I recharge and what shows up in how I collaborate.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-12 grid gap-6 md:grid-cols-3">
|
||||||
|
<article
|
||||||
|
v-for="card in interests"
|
||||||
|
:key="card.title"
|
||||||
|
class="glass-card flex flex-col gap-3 p-6"
|
||||||
|
>
|
||||||
|
<span class="text-2xl" aria-hidden="true">{{ card.emoji }}</span>
|
||||||
|
<h3 class="font-display text-lg font-semibold text-ocean-900 dark:text-white">
|
||||||
|
{{ card.title }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm leading-relaxed text-slate-mist dark:text-coast-foam/80">
|
||||||
|
{{ card.body }}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { projects } from "../content/projects";
|
||||||
|
import { useSectionReveal } from "../composables/useSectionReveal";
|
||||||
|
|
||||||
|
const root = ref<HTMLElement | null>(null);
|
||||||
|
useSectionReveal(root);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
id="projects"
|
||||||
|
ref="root"
|
||||||
|
class="section-inner scroll-mt-24 py-16 sm:py-20"
|
||||||
|
aria-labelledby="projects-heading"
|
||||||
|
>
|
||||||
|
<div class="mx-auto max-w-2xl text-center">
|
||||||
|
<h2
|
||||||
|
id="projects-heading"
|
||||||
|
class="font-display text-3xl font-bold tracking-tight text-ocean-900 dark:text-white sm:text-4xl"
|
||||||
|
>
|
||||||
|
Home lab & demos
|
||||||
|
</h2>
|
||||||
|
<p class="mt-3 text-slate-mist dark:text-coast-foam/75">
|
||||||
|
Selected builds that mirror how I work in the wild: product-minded, infra-aware, AI-curious.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-12 grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<article
|
||||||
|
v-for="p in projects"
|
||||||
|
:key="p.id"
|
||||||
|
class="glass-card flex flex-col p-6 transition hover:-translate-y-0.5 hover:border-sunset-gold/35 hover:shadow-lg dark:hover:border-sunset-gold/30"
|
||||||
|
>
|
||||||
|
<h3 class="font-display text-xl font-semibold text-ocean-900 dark:text-white">
|
||||||
|
{{ p.title }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-2 flex-1 text-sm leading-relaxed text-slate-mist dark:text-coast-foam/80">
|
||||||
|
{{ p.blurb }}
|
||||||
|
</p>
|
||||||
|
<ul class="mt-4 flex flex-wrap gap-2">
|
||||||
|
<li
|
||||||
|
v-for="t in p.stack"
|
||||||
|
:key="t"
|
||||||
|
class="rounded-md bg-ocean-900/5 px-2 py-0.5 text-xs font-medium text-ocean-800 dark:bg-white/5 dark:text-coast-foam/90"
|
||||||
|
>
|
||||||
|
{{ t }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<a
|
||||||
|
:href="p.href"
|
||||||
|
class="focus-ring mt-6 inline-flex items-center gap-2 text-sm font-semibold text-coast-deep dark:text-coast-teal"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ p.cta }}
|
||||||
|
<span aria-hidden="true">→</span>
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import ThemeToggle from "./ThemeToggle.vue";
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{ href: "#hero", label: "Home" },
|
||||||
|
{ href: "#skills", label: "Skills" },
|
||||||
|
{ href: "#projects", label: "Projects" },
|
||||||
|
{ href: "#experience", label: "Experience" },
|
||||||
|
{ href: "#interests", label: "Interests" },
|
||||||
|
{ href: "#contact", label: "Contact" },
|
||||||
|
] as const;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-50 border-b border-ocean-800/10 bg-sand-warm/80 backdrop-blur-lg dark:border-white/5 dark:bg-ocean-950/75"
|
||||||
|
>
|
||||||
|
<div class="section-inner flex h-16 items-center justify-between gap-3">
|
||||||
|
<a
|
||||||
|
href="#hero"
|
||||||
|
class="focus-ring shrink-0 font-display text-lg font-semibold tracking-tight text-ocean-900 dark:text-coast-foam"
|
||||||
|
>
|
||||||
|
Portfolio
|
||||||
|
</a>
|
||||||
|
<nav
|
||||||
|
class="flex min-w-0 flex-1 items-center justify-end gap-2 md:justify-center"
|
||||||
|
aria-label="Primary"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="-mx-1 flex min-w-0 flex-1 gap-1 overflow-x-auto px-1 pb-1 md:flex-initial md:overflow-visible md:pb-0"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-for="l in links"
|
||||||
|
:key="l.href"
|
||||||
|
:href="l.href"
|
||||||
|
class="focus-ring shrink-0 rounded-lg px-2.5 py-2 text-xs font-medium text-slate-mist transition hover:bg-white/60 hover:text-ocean-900 sm:px-3 sm:text-sm dark:hover:bg-white/5 dark:hover:text-white md:px-3"
|
||||||
|
>
|
||||||
|
{{ l.label }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="flex shrink-0 items-center gap-2">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { skillCategories } from "../content/skills";
|
||||||
|
import { useSectionReveal } from "../composables/useSectionReveal";
|
||||||
|
|
||||||
|
const root = ref<HTMLElement | null>(null);
|
||||||
|
useSectionReveal(root);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
id="skills"
|
||||||
|
ref="root"
|
||||||
|
class="section-inner scroll-mt-24 py-16 sm:py-20"
|
||||||
|
aria-labelledby="skills-heading"
|
||||||
|
>
|
||||||
|
<div class="mx-auto max-w-2xl text-center">
|
||||||
|
<h2
|
||||||
|
id="skills-heading"
|
||||||
|
class="font-display text-3xl font-bold tracking-tight text-ocean-900 dark:text-white sm:text-4xl"
|
||||||
|
>
|
||||||
|
Technical skill matrix
|
||||||
|
</h2>
|
||||||
|
<p class="mt-3 text-slate-mist dark:text-coast-foam/75">
|
||||||
|
A quick scan of the stacks I design with, demo, and operate.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-12 grid gap-6 sm:grid-cols-2">
|
||||||
|
<article
|
||||||
|
v-for="cat in skillCategories"
|
||||||
|
:key="cat.id"
|
||||||
|
class="glass-card flex flex-col gap-4 p-6 transition hover:border-coast-deep/30 dark:hover:border-sunset-gold/25"
|
||||||
|
>
|
||||||
|
<h3 class="font-display text-lg font-semibold text-ocean-900 dark:text-white">
|
||||||
|
{{ cat.label }}
|
||||||
|
</h3>
|
||||||
|
<ul class="flex flex-wrap gap-2">
|
||||||
|
<li
|
||||||
|
v-for="item in cat.items"
|
||||||
|
:key="item"
|
||||||
|
class="rounded-full border border-coast-teal/25 bg-coast-teal/10 px-3 py-1 text-xs font-medium text-ocean-800 dark:border-coast-teal/20 dark:bg-coast-deep/25 dark:text-coast-foam"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useTheme } from "../composables/useTheme";
|
||||||
|
|
||||||
|
const { isDark, toggleTheme } = useTheme();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="focus-ring inline-flex h-10 w-10 items-center justify-center rounded-full border border-ocean-800/20 bg-white/70 text-ocean-900 shadow-sm backdrop-blur-md transition hover:border-coast-deep/40 hover:bg-white dark:border-white/10 dark:bg-ocean-900/60 dark:text-coast-foam dark:hover:border-sunset-gold/40"
|
||||||
|
:aria-pressed="isDark"
|
||||||
|
:aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
||||||
|
@click="toggleTheme"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Toggle color theme</span>
|
||||||
|
<!-- Sun -->
|
||||||
|
<svg
|
||||||
|
v-if="isDark"
|
||||||
|
class="h-5 w-5 text-sunset-amber"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.8"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="4" />
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Moon -->
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
class="h-5 w-5 text-ocean-700"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/** Decorative layers — pure CSS, no canvas. */
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none fixed inset-0 -z-10 overflow-hidden"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute -left-1/4 top-0 h-[55vh] w-[150%] bg-gradient-radial from-coast-sky/25 via-transparent to-transparent dark:from-coast-deep/20"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute bottom-0 left-0 right-0 h-[45vh] bg-gradient-to-t from-coast-teal/15 to-transparent dark:from-ocean-800/40"
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
class="absolute bottom-0 left-0 w-[200%] animate-wave text-coast-teal/25 dark:text-coast-deep/35"
|
||||||
|
viewBox="0 0 1440 320"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M0,224L80,213.3C160,203,320,181,480,181.3C640,181,800,203,960,213.3C1120,224,1280,224,1360,224L1440,224L1440,320L1360,320C1280,320,1120,320,960,320C800,320,640,320,480,320C320,320,160,320,80,320L0,320Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="absolute -bottom-8 left-[-10%] w-[220%] animate-wave text-coast-sky/20 dark:text-ocean-700/50"
|
||||||
|
style="animation-delay: -6s"
|
||||||
|
viewBox="0 0 1440 320"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M0,96L60,112C120,128,240,160,360,165.3C480,171,600,149,720,138.7C840,128,960,128,1080,138.7C1200,149,1320,171,1380,181.3L1440,192L1440,320L1380,320C1320,320,1200,320,1080,320C960,320,840,320,720,320C600,320,480,320,360,320C240,320,120,320,60,320L0,320Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- subtle "data" dots -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 opacity-[0.35] dark:opacity-25"
|
||||||
|
style="
|
||||||
|
background-image: radial-gradient(circle at 20% 30%, rgba(45, 212, 191, 0.12) 0, transparent 40%),
|
||||||
|
radial-gradient(circle at 80% 20%, rgba(56, 189, 248, 0.1) 0, transparent 35%),
|
||||||
|
radial-gradient(circle at 70% 80%, rgba(212, 165, 116, 0.08) 0, transparent 45%);
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { type Ref, onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
type RevealTween = {
|
||||||
|
scrollTrigger?: { kill: () => void } | null;
|
||||||
|
kill: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy-loaded GSAP reveal to keep initial bundle smaller.
|
||||||
|
*/
|
||||||
|
export function useSectionReveal(target: Ref<HTMLElement | null>) {
|
||||||
|
let tween: RevealTween | null = null;
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const el = target.value;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const [{ gsap }, { default: ScrollTrigger }] = await Promise.all([
|
||||||
|
import("gsap"),
|
||||||
|
import("gsap/ScrollTrigger"),
|
||||||
|
]);
|
||||||
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
|
|
||||||
|
tween = gsap.from(el, {
|
||||||
|
opacity: 0,
|
||||||
|
y: 28,
|
||||||
|
duration: 0.65,
|
||||||
|
ease: "power2.out",
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: el,
|
||||||
|
start: "top 90%",
|
||||||
|
once: true,
|
||||||
|
},
|
||||||
|
}) as RevealTween;
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
tween?.scrollTrigger?.kill();
|
||||||
|
tween?.kill();
|
||||||
|
tween = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "bio-theme";
|
||||||
|
const isDark = ref(false);
|
||||||
|
|
||||||
|
function applyDarkClass(value: boolean) {
|
||||||
|
if (typeof document === "undefined") return;
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (value) root.classList.add("dark");
|
||||||
|
else root.classList.remove("dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readInitial(): boolean {
|
||||||
|
if (typeof window === "undefined") return false;
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (stored === "dark") return true;
|
||||||
|
if (stored === "light") return false;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Call once before mounting the app to avoid a flash of wrong theme. */
|
||||||
|
export function initTheme() {
|
||||||
|
const dark = readInitial();
|
||||||
|
isDark.value = dark;
|
||||||
|
applyDarkClass(dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
function setTheme(dark: boolean) {
|
||||||
|
isDark.value = dark;
|
||||||
|
applyDarkClass(dark);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, dark ? "dark" : "light");
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
setTheme(!isDark.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isDark, setTheme, toggleTheme };
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
export type Role = {
|
||||||
|
id: string;
|
||||||
|
company: string;
|
||||||
|
title: string;
|
||||||
|
period: string;
|
||||||
|
highlights: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// export const roles: Role[] = [
|
||||||
|
// {
|
||||||
|
// id: "lambdatest",
|
||||||
|
// company: "LambdaTest",
|
||||||
|
// title: "Senior Solutions Engineer (example)",
|
||||||
|
// period: "20XX — Present",
|
||||||
|
// highlights: [
|
||||||
|
// "Led technical discovery workshops and architecture reviews with enterprise teams.",
|
||||||
|
// "Built repeatable demo environments and collateral aligned to buyer journeys.",
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "prior-se",
|
||||||
|
// company: "Previous employer",
|
||||||
|
// title: "Senior Sales Engineer",
|
||||||
|
// period: "20XX — 20XX",
|
||||||
|
// highlights: [
|
||||||
|
// "Owned end-to-end technical sales cycles from discovery to security review.",
|
||||||
|
// "Partnered with product on roadmap feedback from the field.",
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
|
||||||
|
export const roles: Role[] = [
|
||||||
|
{
|
||||||
|
id: "freelance",
|
||||||
|
company: "Independent Contractor / Architect",
|
||||||
|
title: "Full-Stack Developer & Project Manager",
|
||||||
|
period: "2003 — Present",
|
||||||
|
highlights: [
|
||||||
|
"Architected and deployed end-to-end SaaS projects and mobile applications, managing the full development lifecycle. [cite: 393]",
|
||||||
|
"Led technical research into AI/ML models and defined architectural requirements for cloud-native projects. [cite: 394]",
|
||||||
|
"Managed personnel and budgets for diverse technical projects to ensure on-time delivery. [cite: 395]",
|
||||||
|
"Utilized AWS and GCP for scalable infrastructure and high-performance React/Python codebases. [cite: 396, 397]"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "pubnub",
|
||||||
|
company: "PubNub Inc",
|
||||||
|
title: "Senior Solutions Architect",
|
||||||
|
period: "2021 — 2025",
|
||||||
|
highlights: [
|
||||||
|
"Served as the lead technical expert for pre-sales and post-sales engagements with enterprise and startup clients. [cite: 401]",
|
||||||
|
"Consulted on SDK usage and architectural design patterns for Fintech, MedTech, and IoT industries. [cite: 402]",
|
||||||
|
"Translated complex customer requirements into actionable product feedback for Engineering and Product teams. [cite: 404]",
|
||||||
|
"Conducted technical deep-dives and security reviews for C-level executives and engineering teams. [cite: 405]",
|
||||||
|
"Represented the company at industry trade shows to demonstrate the value of real-time communication PaaS. [cite: 301]"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ebrevia",
|
||||||
|
company: "eBrevia LLC",
|
||||||
|
title: "Sales Engineer",
|
||||||
|
period: "2020 — 2021",
|
||||||
|
highlights: [
|
||||||
|
"Secured major client accounts through technical RFP/RFI assessments and compliance documentation. [cite: 411]",
|
||||||
|
"Addressed complex security and product architecture concerns during SOC 2 compliance reviews. [cite: 412]",
|
||||||
|
"Bridged communication between clients and product teams to facilitate custom feature implementations. [cite: 413]",
|
||||||
|
"Delivered compelling demonstrations of machine learning capabilities for legal and technical stakeholders. [cite: 414]"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "priceline",
|
||||||
|
company: "Priceline.com LLC",
|
||||||
|
title: "Senior Software Engineer / Lead Engineer",
|
||||||
|
period: "2017 — 2019",
|
||||||
|
highlights: [
|
||||||
|
"Directed front-end development for the company's first internationalization (i18n) and localization effort. [cite: 418]",
|
||||||
|
"Spearheaded the migration of legacy Angular applications to a modern React/Redux framework. [cite: 420]",
|
||||||
|
"Coordinated server migrations to the cloud and analyzed impacts on global system performance. [cite: 70]",
|
||||||
|
"Presented strategic findings on internationalization best practices to the CTO and technical leadership. [cite: 421]",
|
||||||
|
"Mentored junior engineers in maintaining core products and adopting lean development practices. [cite: 71, 72]"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dealflicks",
|
||||||
|
company: "Dealflicks Inc.",
|
||||||
|
title: "Software/Web Developer & System Admin",
|
||||||
|
period: "2013 — 2016",
|
||||||
|
highlights: [
|
||||||
|
"Engineered and managed a full-stack website on AWS using Python frameworks Tornado and Django. [cite: 81]",
|
||||||
|
"Developed a RESTful API in Go and an automated movie showtime scraping feeder. [cite: 83, 84]",
|
||||||
|
"Built a gift card product catalog and an automated drip email campaign system to drive engagement. [cite: 85, 98]",
|
||||||
|
"Managed cloud deployments via Fabric and configured secure VPC/VPN staging environments. [cite: 87, 88]",
|
||||||
|
"Developed a partner web portal using the Java/Scala Play Framework for theater performance metrics. [cite: 94]"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "canara",
|
||||||
|
company: "Canara Inc. (formerly Intellibatt)",
|
||||||
|
title: "Software & Web Developer",
|
||||||
|
period: "2012 — 2013",
|
||||||
|
highlights: [
|
||||||
|
"Engineered real-time NOC sites for continuous alarm reporting and massive power data stream monitoring. [cite: 106, 107]",
|
||||||
|
"Built and deployed co-location servers and maintained network firewalls using pfSense. [cite: 101, 102]",
|
||||||
|
"Developed automated data collection scripts for network mini-computers monitoring power lines. [cite: 109]",
|
||||||
|
"Maintained large-scale databases and executed complex queries for cross-departmental operations. [cite: 110]",
|
||||||
|
"Resolved internal IT infrastructure issues across mailboxes, telephony, and computing systems. [cite: 103]"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "intrepid",
|
||||||
|
company: "Intrepid / Anonymizer",
|
||||||
|
title: "System Engineer / Web Developer",
|
||||||
|
period: "2005 — 2012",
|
||||||
|
highlights: [
|
||||||
|
"Developed and maintained highly secure web applications focused on anonymity and information assurance. [cite: 190, 276]",
|
||||||
|
"Architected secure network infrastructures utilizing encryption standards such as AES, SSL/TLS, and RSA. [cite: 139, 198]",
|
||||||
|
"Managed and configured complex routing and firewall environments with pfSense and Juniper JUNOS. [cite: 134, 135, 202]",
|
||||||
|
"Collaborated on specialized hardware and software monitoring solutions for high-security environments. [cite: 198, 276]"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export type Project = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
blurb: string;
|
||||||
|
stack: string[];
|
||||||
|
href: string;
|
||||||
|
cta: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const projects: Project[] = [
|
||||||
|
{
|
||||||
|
id: "lorekeeper",
|
||||||
|
title: "LoreKeeper",
|
||||||
|
blurb: "Story-writing assistant combining Vue, Python, and AI workflows.",
|
||||||
|
stack: ["Vue", "Python", "AI"],
|
||||||
|
href: "#",
|
||||||
|
cta: "View project",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ha",
|
||||||
|
title: "Home Assistant Dashboard",
|
||||||
|
blurb: "Custom Vue 3 dashboard for home automation visibility and control.",
|
||||||
|
stack: ["Vue 3", "HA API"],
|
||||||
|
href: "#",
|
||||||
|
cta: "Live demo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "demos",
|
||||||
|
title: "Test Demo Sites",
|
||||||
|
blurb: "Proof-of-concepts and experiments hosted on subdomains or lab containers.",
|
||||||
|
stack: ["Containers", "Tunneling"],
|
||||||
|
href: "#",
|
||||||
|
cta: "Browse demos",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/** Replace with your public URLs and copy. */
|
||||||
|
export const site = {
|
||||||
|
name: "Matthew Edwards",
|
||||||
|
titleLine: "Software Developer | Solutions Architect",
|
||||||
|
tagline:
|
||||||
|
"I bridge product vision and reliable systems — from developing full-stack applications and architecting scalable systems, to understanding business needs and offering solutions that meet them.",
|
||||||
|
email: "matthew.ed@gmail.com",
|
||||||
|
linkedinUrl: "https://www.linkedin.com/in/matthew-edwards-643ba95/",
|
||||||
|
githubUrl: "https://github.com/lvbronco/",
|
||||||
|
ogUrl: "https://beachho.us",
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
export type SkillCategory = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
items: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// export const skillCategories: SkillCategory[] = [
|
||||||
|
// {
|
||||||
|
// id: "core",
|
||||||
|
// label: "Core Tech",
|
||||||
|
// items: ["Vue.js", "TypeScript", "Python", "Node.js", "REST APIs"],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "infra",
|
||||||
|
// label: "Infrastructure",
|
||||||
|
// items: ["Proxmox", "Docker", "WireGuard", "Linux", "CI/CD"],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "ai",
|
||||||
|
// label: "AI / LLM",
|
||||||
|
// items: ["Gemini API", "Ollama", "RAG patterns", "Prompt tooling"],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "sales",
|
||||||
|
// label: "Sales Engineering",
|
||||||
|
// items: ["Technical demos", "PRD creation", "Solution architecture", "POC delivery"],
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
|
||||||
|
export const skillCategories: SkillCategory[] = [
|
||||||
|
{
|
||||||
|
id: "core",
|
||||||
|
label: "Core Tech",
|
||||||
|
items: [
|
||||||
|
"Python (Async/Pydantic)", // The non-negotiable base for AI engineering in 2026
|
||||||
|
"React & TypeScript", // Current industry standard for type-safe frontends
|
||||||
|
"i18n & Global Expansion", // Your unique enterprise differentiator
|
||||||
|
"Go (Microservices)", // High-performance backend trend
|
||||||
|
"System Design & Scalability", // Critical for Senior/Lead level assessment
|
||||||
|
"REST & GraphQL", // Modern API connectivity standards
|
||||||
|
"Java/Scala (Play Framework)", //
|
||||||
|
"Performance Optimization", // Crucial for user retention and SEO
|
||||||
|
"Unit/Integration Testing", //
|
||||||
|
"Cross-Platform Development", //
|
||||||
|
"Modern CSS/Tailwind", //
|
||||||
|
"Legacy Migration Strategy" // A key business-value skill for senior devs
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "infra",
|
||||||
|
label: "Infrastructure",
|
||||||
|
items: [
|
||||||
|
"Platform Engineering", // 2026 trend moving beyond basic DevOps
|
||||||
|
"AWS & GCP Cloud Native", // Core demand for cloud-first organizations
|
||||||
|
"Infrastructure as Code (IaC)", // Managing systems via code (Terraform/Ansible)
|
||||||
|
"Proxmox & Home Lab", // Your advanced self-hosted edge
|
||||||
|
"Kubernetes & Docker", // Now a "required" skill for most infra roles
|
||||||
|
"Zero-Trust Networking", // Modern security trend for distributed teams
|
||||||
|
"CI/CD & GitOps", // Automated deployment standard
|
||||||
|
"SRE & High Availability", // Merging ops and software for resilience
|
||||||
|
"Cloud Security (DevSecOps)", // Integrating security into the dev pipeline
|
||||||
|
"Monitoring (Splunk/ELK)", //
|
||||||
|
"Serverless Computing", //
|
||||||
|
"Hybrid-Cloud Orchestration" //
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ai",
|
||||||
|
label: "AI / LLM",
|
||||||
|
items: [
|
||||||
|
"Agentic AI Frameworks", // Using LangChain/LangGraph for autonomous systems
|
||||||
|
"MCP (Model Context Protocol)", // The 2026 standard for connecting LLMs to tools
|
||||||
|
"RAG & Data Activation", // Efficient retrieval-augmented generation
|
||||||
|
"Prompt Engineering", // Refining model behavior
|
||||||
|
"MLOps & Model Deployment", // Operationalizing AI at scale
|
||||||
|
"Gemini & OpenAI SDKs", // Leading API platforms
|
||||||
|
"Local LLM Optimization", // Privacy and cost-focused local execution
|
||||||
|
"Structured Output Mastery", // Using Pydantic for reliable AI data
|
||||||
|
"AI Security & Ethics", // Managing bias and safety
|
||||||
|
"Vector DBs (Pinecone/Chroma)", //
|
||||||
|
"Function Calling & Tool Use", //
|
||||||
|
"AI Product Management" // Bridging the gap from model to product
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sales",
|
||||||
|
label: "Sales Engineering",
|
||||||
|
items: [
|
||||||
|
"Solution Storytelling", // Translating features into business outcomes
|
||||||
|
"Technical Discovery", // Deep qualification of customer needs
|
||||||
|
"SOC 2 & Cybersecurity Reviews", // Critical for closing enterprise deals
|
||||||
|
"POC Delivery & Management", // Demonstrating value in live environments
|
||||||
|
"RFP/RFI Technical Strategy", // Winning high-stakes competitive bids
|
||||||
|
"Consultative Selling", // Acting as a trusted technical advisor
|
||||||
|
"Stakeholder Management", // Navigating complex political dynamics in accounts
|
||||||
|
"Cross-Functional Liaison", // Linking Sales, Product, and Engineering
|
||||||
|
"Public Speaking & Demos", // Trade shows and executive presentations
|
||||||
|
"API & Integration Strategy", //
|
||||||
|
"CRM & Pipeline Hygiene", // Strategic use of Salesforce
|
||||||
|
"Sales Enablement Content" // Creating guides and demos that scale
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import App from "./App.vue";
|
||||||
|
import { initTheme } from "./composables/useTheme";
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
initTheme();
|
||||||
|
createApp(App).mount("#app");
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply font-sans antialiased text-slate-ink bg-sand-warm dark:bg-ocean-950 dark:text-coast-foam;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
@apply bg-coast-teal/30 text-ocean-950;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::selection {
|
||||||
|
@apply bg-sunset-gold/30 text-ocean-950;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.glass-card {
|
||||||
|
@apply rounded-2xl border border-white/40 bg-white/55 shadow-glass backdrop-blur-xl dark:border-white/10 dark:bg-ocean-900/50 dark:shadow-glass-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-inner {
|
||||||
|
@apply mx-auto w-full max-w-6xl px-4 sm:px-6 lg:px-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-ring {
|
||||||
|
@apply focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-coast-deep dark:focus-visible:outline-sunset-gold;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module "*.vue" {
|
||||||
|
import type { DefineComponent } from "vue";
|
||||||
|
const component: DefineComponent<object, object, unknown>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
ocean: {
|
||||||
|
// Derived from Logo Blue (#075985)
|
||||||
|
950: "#021526", // Deep midnight base
|
||||||
|
900: "#032d45", // Darker navy
|
||||||
|
800: "#054161", // Deep ocean
|
||||||
|
700: "#064e75", // Slightly darker than logo
|
||||||
|
600: "#075985", // [LOGO MATCH: Ocean Blue]
|
||||||
|
500: "#0ea5e9", // Bright accent
|
||||||
|
},
|
||||||
|
slate: {
|
||||||
|
ink: "#0f172a", // Darker, cooler slate to match the new Ocean 950
|
||||||
|
mist: "#94a3b8", // Lightened for better contrast on dark blues
|
||||||
|
},
|
||||||
|
coast: {
|
||||||
|
teal: "#2dd4bf", // Keep for pop
|
||||||
|
deep: "#0d9488",
|
||||||
|
sky: "#38bdf8",
|
||||||
|
foam: "#E0F2FE", // [LOGO MATCH: Seafoam/Sky]
|
||||||
|
},
|
||||||
|
sand: {
|
||||||
|
DEFAULT: "#D2B48C", // [LOGO MATCH: Sand]
|
||||||
|
warm: "#f3e1c7", // Lighter tint for backgrounds/cards
|
||||||
|
},
|
||||||
|
sunset: {
|
||||||
|
gold: "#FB8524", // [LOGO MATCH: Sunset Gold]
|
||||||
|
amber: "#f59e0b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// colors: {
|
||||||
|
// ocean: {
|
||||||
|
// 950: "#0a1628",
|
||||||
|
// 900: "#0f2137",
|
||||||
|
// 800: "#152a45",
|
||||||
|
// 700: "#1c3554",
|
||||||
|
// 600: "#244468",
|
||||||
|
// },
|
||||||
|
// slate: {
|
||||||
|
// ink: "#1e293b",
|
||||||
|
// mist: "#64748b",
|
||||||
|
// },
|
||||||
|
// coast: {
|
||||||
|
// teal: "#2dd4bf",
|
||||||
|
// deep: "#0d9488",
|
||||||
|
// sky: "#38bdf8",
|
||||||
|
// foam: "#7dd3fc",
|
||||||
|
// },
|
||||||
|
// sand: {
|
||||||
|
// DEFAULT: "#e8d5b7",
|
||||||
|
// warm: "#f4e4c1",
|
||||||
|
// },
|
||||||
|
// sunset: {
|
||||||
|
// gold: "#d4a574",
|
||||||
|
// amber: "#f59e0b",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
fontFamily: {
|
||||||
|
sans: [
|
||||||
|
"DM Sans",
|
||||||
|
"system-ui",
|
||||||
|
"-apple-system",
|
||||||
|
"Segoe UI",
|
||||||
|
"Roboto",
|
||||||
|
"sans-serif",
|
||||||
|
],
|
||||||
|
display: ["Outfit", "DM Sans", "system-ui", "sans-serif"],
|
||||||
|
mono: ["JetBrains Mono", "ui-monospace", "monospace"],
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
glass: "0 8px 32px rgba(15, 33, 55, 0.12)",
|
||||||
|
"glass-dark": "0 8px 32px rgba(0, 0, 0, 0.35)",
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
wave: "wave 18s ease-in-out infinite",
|
||||||
|
float: "float 6s ease-in-out infinite",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
wave: {
|
||||||
|
"0%, 100%": { transform: "translateX(0) scaleY(1)" },
|
||||||
|
"50%": { transform: "translateX(-4%) scaleY(1.02)" },
|
||||||
|
},
|
||||||
|
float: {
|
||||||
|
"0%, 100%": { transform: "translateY(0)" },
|
||||||
|
"50%": { transform: "translateY(-6px)" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
} satisfies Config;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
|
||||||
|
function viteBase(): string {
|
||||||
|
const raw = process.env.VITE_BASE?.trim();
|
||||||
|
if (!raw) return "/";
|
||||||
|
return raw.endsWith("/") ? raw : `${raw}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For GitHub Pages project sites: VITE_BASE=/repo-name/ (trailing slash optional)
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
base: viteBase(),
|
||||||
|
});
|
||||||
Reference in new issue
Block a user