316 lines
8.2 KiB
Markdown
316 lines
8.2 KiB
Markdown
|
|
# CMD+K Command Palette API Documentation
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
The CV application provides a command palette (CMD+K / Ctrl+K) powered by [ninja-keys](https://github.com/nickadam/ninja-keys) web component. Dynamic entries (experiences, projects, courses) are loaded from a backend API endpoint, allowing automatic updates when CV data changes without modifying JavaScript code.
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
### Design Decision
|
||
|
|
|
||
|
|
**API-First Approach**: Rather than hardcoding entries in JavaScript or reading from DOM elements, the command palette fetches its dynamic data from a dedicated API endpoint. This provides:
|
||
|
|
|
||
|
|
1. **Automatic Updates**: New CV entries appear in CMD+K without code changes
|
||
|
|
2. **Language Support**: API returns localized data based on language parameter
|
||
|
|
3. **Cache Efficiency**: 1-hour cache headers reduce redundant requests
|
||
|
|
4. **Separation of Concerns**: Frontend only handles rendering; backend owns data
|
||
|
|
|
||
|
|
### Data Flow
|
||
|
|
|
||
|
|
```
|
||
|
|
User opens CMD+K (Ctrl+K / Cmd+K)
|
||
|
|
↓
|
||
|
|
ninja-keys-init.js initializes
|
||
|
|
↓
|
||
|
|
fetch('/api/cmd-k?lang={en|es}')
|
||
|
|
↓
|
||
|
|
Backend loads CV data from JSON files
|
||
|
|
↓
|
||
|
|
Maps experiences, projects, courses to actions
|
||
|
|
↓
|
||
|
|
Returns JSON with action arrays
|
||
|
|
↓
|
||
|
|
Frontend combines with static actions
|
||
|
|
↓
|
||
|
|
ninja-keys displays searchable command palette
|
||
|
|
```
|
||
|
|
|
||
|
|
## API Endpoint
|
||
|
|
|
||
|
|
### GET /api/cmd-k
|
||
|
|
|
||
|
|
Returns dynamic entries for the ninja-keys command palette.
|
||
|
|
|
||
|
|
**URL**: `/api/cmd-k`
|
||
|
|
**Method**: `GET`
|
||
|
|
**Authentication**: None (public endpoint)
|
||
|
|
|
||
|
|
#### Query Parameters
|
||
|
|
|
||
|
|
| Parameter | Type | Default | Description |
|
||
|
|
|-----------|------|---------|-------------|
|
||
|
|
| `lang` | string | `en` | Language code (`en` or `es`) |
|
||
|
|
|
||
|
|
#### Response
|
||
|
|
|
||
|
|
**Content-Type**: `application/json`
|
||
|
|
**Cache-Control**: `public, max-age=3600` (1 hour)
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"experiences": [
|
||
|
|
{
|
||
|
|
"id": "exp-olympic-broadcasting",
|
||
|
|
"title": "Olympic Broadcasting Services",
|
||
|
|
"section": "Experience",
|
||
|
|
"keywords": "Olympic Broadcasting Services Senior SAP Technical Consultant"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"projects": [
|
||
|
|
{
|
||
|
|
"id": "proj-somos-una-ola",
|
||
|
|
"title": "Somos Una Ola",
|
||
|
|
"section": "Projects",
|
||
|
|
"keywords": "Somos Una Ola Volunteer project promoting beach cleaning..."
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"courses": [
|
||
|
|
{
|
||
|
|
"id": "course-codecademy-certifications",
|
||
|
|
"title": "Codecademy Certifications",
|
||
|
|
"section": "Courses",
|
||
|
|
"keywords": "Codecademy Certifications Codecademy"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Response Fields
|
||
|
|
|
||
|
|
| Field | Type | Description |
|
||
|
|
|-------|------|-------------|
|
||
|
|
| `experiences` | array | Work experience entries |
|
||
|
|
| `projects` | array | Personal/professional project entries |
|
||
|
|
| `courses` | array | Course and certification entries |
|
||
|
|
|
||
|
|
Each entry contains:
|
||
|
|
|
||
|
|
| Field | Type | Description |
|
||
|
|
|-------|------|-------------|
|
||
|
|
| `id` | string | Unique identifier (e.g., `exp-{companyId}`, `proj-{projectId}`) |
|
||
|
|
| `title` | string | Display title for the command palette |
|
||
|
|
| `section` | string | Section label (`Experience`, `Projects`, `Courses`) |
|
||
|
|
| `keywords` | string | Searchable keywords for filtering |
|
||
|
|
|
||
|
|
#### Example Requests
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# English (default)
|
||
|
|
curl http://localhost:1999/api/cmd-k
|
||
|
|
|
||
|
|
# Spanish
|
||
|
|
curl http://localhost:1999/api/cmd-k?lang=es
|
||
|
|
|
||
|
|
# With jq formatting
|
||
|
|
curl -s http://localhost:1999/api/cmd-k | jq '.'
|
||
|
|
|
||
|
|
# Check response headers
|
||
|
|
curl -I http://localhost:1999/api/cmd-k
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Error Responses
|
||
|
|
|
||
|
|
| Status | Description |
|
||
|
|
|--------|-------------|
|
||
|
|
| 500 | Failed to load CV data |
|
||
|
|
|
||
|
|
## Frontend Integration
|
||
|
|
|
||
|
|
### ninja-keys-init.js
|
||
|
|
|
||
|
|
The frontend JavaScript fetches from the API and combines with static actions:
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// Fetch dynamic entries from API
|
||
|
|
async function fetchDynamicEntries() {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/cmd-k?lang=${lang}`);
|
||
|
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||
|
|
return await response.json();
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to fetch CMD+K data:', error);
|
||
|
|
return { experiences: [], projects: [], courses: [] };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Combine with static actions
|
||
|
|
const dynamicData = await fetchDynamicEntries();
|
||
|
|
const actions = [
|
||
|
|
...staticActions,
|
||
|
|
...mapExperienceActions(dynamicData.experiences || []),
|
||
|
|
...mapProjectActions(dynamicData.projects || []),
|
||
|
|
...mapCourseActions(dynamicData.courses || [])
|
||
|
|
];
|
||
|
|
|
||
|
|
ninjaKeys.data = actions;
|
||
|
|
```
|
||
|
|
|
||
|
|
### Action Mapping
|
||
|
|
|
||
|
|
Dynamic entries are converted to ninja-keys actions with handlers:
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
function mapExperienceActions(experiences) {
|
||
|
|
return experiences.map(exp => ({
|
||
|
|
id: exp.id,
|
||
|
|
title: exp.title,
|
||
|
|
section: exp.section,
|
||
|
|
keywords: `${exp.keywords} work job career`.toLowerCase(),
|
||
|
|
icon: '<iconify-icon icon="mdi:office-building" width="20"></iconify-icon>',
|
||
|
|
handler: () => scrollToSection(exp.id)
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Backend Implementation
|
||
|
|
|
||
|
|
### Handler: cv_cmdk.go
|
||
|
|
|
||
|
|
```go
|
||
|
|
// CmdKData returns JSON data for the ninja-keys command palette
|
||
|
|
func (h *CVHandler) CmdKData(w http.ResponseWriter, r *http.Request) {
|
||
|
|
lang := r.URL.Query().Get("lang")
|
||
|
|
if lang == "" {
|
||
|
|
lang = "en"
|
||
|
|
}
|
||
|
|
if lang != "en" && lang != "es" {
|
||
|
|
lang = "en"
|
||
|
|
}
|
||
|
|
|
||
|
|
cv, err := models.LoadCV(lang)
|
||
|
|
if err != nil {
|
||
|
|
http.Error(w, "Failed to load CV data", http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
response := CmdKResponse{
|
||
|
|
Experiences: mapExperiences(cv.Experience),
|
||
|
|
Projects: mapProjects(cv.Projects),
|
||
|
|
Courses: mapCourses(cv.Courses),
|
||
|
|
}
|
||
|
|
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
w.Header().Set("Cache-Control", "public, max-age=3600")
|
||
|
|
json.NewEncoder(w).Encode(response)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Route Registration
|
||
|
|
|
||
|
|
```go
|
||
|
|
// routes/routes.go
|
||
|
|
// API routes (must be before "/" to avoid catch-all)
|
||
|
|
mux.HandleFunc("/api/cmd-k", cvHandler.CmdKData)
|
||
|
|
```
|
||
|
|
|
||
|
|
## ID Convention
|
||
|
|
|
||
|
|
IDs follow a consistent pattern matching DOM element IDs for scroll targeting:
|
||
|
|
|
||
|
|
| Type | Pattern | Example |
|
||
|
|
|------|---------|---------|
|
||
|
|
| Experience | `exp-{companyId}` | `exp-olympic-broadcasting` |
|
||
|
|
| Project | `proj-{projectId}` | `proj-somos-una-ola` |
|
||
|
|
| Course | `course-{courseId}` | `course-codecademy-certifications` |
|
||
|
|
|
||
|
|
These IDs correspond to HTML element IDs in the page:
|
||
|
|
```html
|
||
|
|
<div class="experience-item" id="exp-olympic-broadcasting">...</div>
|
||
|
|
<div class="project-item" id="proj-somos-una-ola">...</div>
|
||
|
|
<div class="course-item" id="course-codecademy-certifications">...</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
## Static Actions
|
||
|
|
|
||
|
|
In addition to dynamic entries, the command palette includes static actions:
|
||
|
|
|
||
|
|
### Navigation
|
||
|
|
- Jump to Top, Experience, Education, Skills, Projects, Courses, Languages, Awards, Other Info
|
||
|
|
|
||
|
|
### Shortcuts
|
||
|
|
- Toggle CV Length (L key)
|
||
|
|
- Toggle Icons (I key)
|
||
|
|
- Toggle Theme (V key)
|
||
|
|
- Show Shortcuts Help (? key)
|
||
|
|
- Print CV (Cmd+P)
|
||
|
|
|
||
|
|
### Downloads
|
||
|
|
- Download PDF (Default, Short, Extended versions)
|
||
|
|
- View/Download Text CV
|
||
|
|
|
||
|
|
### Actions
|
||
|
|
- Open Contact Form
|
||
|
|
- Show Site Info
|
||
|
|
- Toggle Zoom Controls
|
||
|
|
- Switch Language (EN/ES)
|
||
|
|
- Change Color Theme
|
||
|
|
|
||
|
|
### Social Links
|
||
|
|
- LinkedIn, GitHub, Domestika, Personal Website
|
||
|
|
|
||
|
|
## Testing
|
||
|
|
|
||
|
|
### Unit Tests (Go)
|
||
|
|
|
||
|
|
Located at `internal/handlers/cv_cmdk_test.go`:
|
||
|
|
|
||
|
|
```go
|
||
|
|
func TestCmdKData(t *testing.T) {
|
||
|
|
// Tests: Default language, English, Spanish, Invalid language fallback
|
||
|
|
// Validates: Status code, Content-Type, response structure, counts
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCmdKDataCaching(t *testing.T) {
|
||
|
|
// Validates Cache-Control header
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Run with:
|
||
|
|
```bash
|
||
|
|
go test ./internal/handlers/ -run TestCmdK -v
|
||
|
|
```
|
||
|
|
|
||
|
|
### E2E Tests (Playwright/Bun)
|
||
|
|
|
||
|
|
Located at `tests/mjs/71-cmd-k-api-scroll.test.mjs`:
|
||
|
|
|
||
|
|
Tests:
|
||
|
|
1. API returns valid JSON with expected structure
|
||
|
|
2. Experience scroll navigation works
|
||
|
|
3. Project scroll navigation works
|
||
|
|
4. Course scroll navigation works
|
||
|
|
5. Section scroll navigation works
|
||
|
|
6. Multiple sequential searches work correctly
|
||
|
|
|
||
|
|
Run with:
|
||
|
|
```bash
|
||
|
|
HEADLESS=true bun run tests/mjs/71-cmd-k-api-scroll.test.mjs
|
||
|
|
```
|
||
|
|
|
||
|
|
## Performance
|
||
|
|
|
||
|
|
- **Cache Duration**: 1 hour (reduces API calls on page refresh)
|
||
|
|
- **Response Size**: ~2-3 KB (compact JSON)
|
||
|
|
- **Load Time**: API fetched during page initialization
|
||
|
|
- **Fallback**: Empty arrays returned on error (graceful degradation)
|
||
|
|
|
||
|
|
## Files
|
||
|
|
|
||
|
|
| File | Purpose |
|
||
|
|
|------|---------|
|
||
|
|
| `internal/handlers/cv_cmdk.go` | API handler |
|
||
|
|
| `internal/handlers/cv_cmdk_test.go` | Unit tests |
|
||
|
|
| `internal/routes/routes.go` | Route registration |
|
||
|
|
| `static/js/ninja-keys-init.js` | Frontend integration |
|
||
|
|
| `tests/mjs/71-cmd-k-api-scroll.test.mjs` | E2E tests |
|