diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/.htaccess @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.memory/hanko-seal.md b/.memory/hanko-seal.md new file mode 100644 index 0000000..a26a772 --- /dev/null +++ b/.memory/hanko-seal.md @@ -0,0 +1,79 @@ +# The Hanko Seal — concept note + +**Status:** Concept, partially implemented · **Date:** 2026-07-22 +**Related:** [brand/BRAND.md](../brand/BRAND.md) · [brand/tokens.css](../brand/tokens.css) + +--- + +## What it is + +The small vermillion square beside the wordmark is a **hanko** (判子) — a +Japanese seal, also called an *inkan* (印鑑). The character on it, **印**, is +read *in* and literally means "seal, stamp, mark." + +When a seal appears on artwork next to the artist's signature, the signature +and seal together are a **rakkan** (落款) — the traditional finishing gesture +on ink paintings and calligraphy. You sign the work, then press your seal in +red. + +The red is not decorative. Seal ink is a vermillion paste called **shuniku** +(朱肉), which is where the brand red comes from — `#D0342C` is named Shuniku in +the brand guide for exactly this reason. + +## Why it fits this brand + +The logo is a handwritten signature: a brush stroke. A seal is what completes a +brush stroke. The rakkan pairing already existed in the identity before the +sumi-e direction was chosen — the seal only made it explicit. + +It is also the origin of the **1–2% coverage rule** in the brand guide. A seal +is small, pressed once, and means *this is finished, and it is mine*. Spread +the same red across a page and it stops reading as a seal and starts reading as +a generic accent colour. The restraint is not stylistic caution; it is what +makes the mark mean something. + +## Where it currently appears + +- Beside the signature wordmark in the site header +- Top-left of the full-page mobile menu +- Previously bottom-right of the hero portrait (removed with the photo) + +## Open — the thing to do later + +**印 is a placeholder.** A traditional hanko carries its owner's *name*, not a +generic character. Using 印 is close to signing a painting with the word +"signature." It reads well, but it is not yet personal. + +Replacing it solves a second, unrelated problem already logged in the brand +guide: the signature wordmark is roughly 4:1, so it cannot serve as a favicon, +social avatar, or app icon — it becomes an illegible smear at 32px. A seal is +square and legible at any size. + +Directions worth exploring when this is picked up: + +1. **Initials in seal script** (篆書, *tensho*) — the archaic script style + traditionally used on hanko. Most authentic; needs care to stay legible to a + Western audience. +2. **A stylised C** carved in negative space, in the manner of a *hakubun* + (white-character) seal, where the character is cut away and the ground + prints red. +3. **カルロス** (Karurosu) — the name in katakana, which is how a non-Japanese + name is conventionally rendered on a personal seal. +4. **A mark rather than a character** — abstract, derived from the signature's + own stroke geometry. + +Whichever is chosen becomes the favicon, the avatar, and the small-size lockup +across every surface. It is a brand-defining decision, not a detail — worth +doing deliberately rather than as a quick swap. + +## Implementation notes + +The seal is rendered as a token-driven element, not an image: + +- `.ca-header-seal` and `.ca-menu-seal` in `app/src/styles.css` +- Colour comes from `--ca-seal`, which shifts to `--ca-ink-surface-seal` + (`#E8564A`) on ink grounds — `#D0342C` muddies against near-black +- Square by design: `--radius` is `2px`, near-square rather than rounded + +Swapping the placeholder for a real mark means replacing the character with an +SVG, keeping the same token-driven colour behaviour. diff --git a/.pre-seed-backup/.htaccess b/.pre-seed-backup/.htaccess new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/.pre-seed-backup/.htaccess @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.pre-seed-backup/.user.ini b/.pre-seed-backup/.user.ini new file mode 100644 index 0000000..d411df0 --- /dev/null +++ b/.pre-seed-backup/.user.ini @@ -0,0 +1 @@ +open_basedir=/www/wwwroot/CarlosAriasPersonal/:/tmp/ \ No newline at end of file diff --git a/.pre-seed-backup/404.html b/.pre-seed-backup/404.html new file mode 100644 index 0000000..a2f560c --- /dev/null +++ b/.pre-seed-backup/404.html @@ -0,0 +1,74 @@ + + + + + + 404 Not Found + + + +
+ +
404
+
Sorry, the page you visited does not exist.
It may be that the access link is wrong or the file does not exist.
+ +
+ + diff --git a/.pre-seed-backup/502.html b/.pre-seed-backup/502.html new file mode 100644 index 0000000..5f25659 --- /dev/null +++ b/.pre-seed-backup/502.html @@ -0,0 +1,74 @@ + + + + + + 502 Bad Gateway + + + +
+ +
502
+
Gateway error.
The website server has timed out in returning data, please wait a few minutes and try again.
+ +
+ + diff --git a/.pre-seed-backup/index.html b/.pre-seed-backup/index.html new file mode 100644 index 0000000..1bd54ae --- /dev/null +++ b/.pre-seed-backup/index.html @@ -0,0 +1,55 @@ + + + + + Site is created successfully! + + + +
+

Congratulations, the site is created successfully!

+

This is the default index.html, this page is automatically generated by the system

+ +
+ + + \ No newline at end of file diff --git a/.reference_files/Carlos Arias 2025 - Master Resume 2026 (1).pdf b/.reference_files/Carlos Arias 2025 - Master Resume 2026 (1).pdf new file mode 100644 index 0000000..1207744 Binary files /dev/null and b/.reference_files/Carlos Arias 2025 - Master Resume 2026 (1).pdf differ diff --git a/.reference_files/CarlosArias-Brand-Logo.svg b/.reference_files/CarlosArias-Brand-Logo.svg new file mode 100644 index 0000000..69d9eb7 --- /dev/null +++ b/.reference_files/CarlosArias-Brand-Logo.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.reference_files/CarlosArias-Resume-Dark.png b/.reference_files/CarlosArias-Resume-Dark.png new file mode 100644 index 0000000..16afed9 Binary files /dev/null and b/.reference_files/CarlosArias-Resume-Dark.png differ diff --git a/.reference_files/CarlosArias-Resume-Light.png b/.reference_files/CarlosArias-Resume-Light.png new file mode 100644 index 0000000..e805196 Binary files /dev/null and b/.reference_files/CarlosArias-Resume-Light.png differ diff --git a/.reference_files/CarlosAriasAgentic.png b/.reference_files/CarlosAriasAgentic.png new file mode 100644 index 0000000..b31ee95 Binary files /dev/null and b/.reference_files/CarlosAriasAgentic.png differ diff --git a/.reference_files/CarlosAriasOrbs.png b/.reference_files/CarlosAriasOrbs.png new file mode 100644 index 0000000..493bc4f Binary files /dev/null and b/.reference_files/CarlosAriasOrbs.png differ diff --git a/.reference_files/WhatsApp Image 2026-07-21 at 8.21.24 PM.jpeg b/.reference_files/WhatsApp Image 2026-07-21 at 8.21.24 PM.jpeg new file mode 100644 index 0000000..8bf4e97 Binary files /dev/null and b/.reference_files/WhatsApp Image 2026-07-21 at 8.21.24 PM.jpeg differ diff --git a/.reference_files/WhatsApp Image 2026-07-21 at 8.21.47 PM.jpeg b/.reference_files/WhatsApp Image 2026-07-21 at 8.21.47 PM.jpeg new file mode 100644 index 0000000..f90ad95 Binary files /dev/null and b/.reference_files/WhatsApp Image 2026-07-21 at 8.21.47 PM.jpeg differ diff --git a/.reference_files/WhatsApp Image 2026-07-21 at 8.23.10 PM (1).jpeg b/.reference_files/WhatsApp Image 2026-07-21 at 8.23.10 PM (1).jpeg new file mode 100644 index 0000000..271927b Binary files /dev/null and b/.reference_files/WhatsApp Image 2026-07-21 at 8.23.10 PM (1).jpeg differ diff --git a/.reference_files/WhatsApp Image 2026-07-21 at 8.23.10 PM (2).jpeg b/.reference_files/WhatsApp Image 2026-07-21 at 8.23.10 PM (2).jpeg new file mode 100644 index 0000000..1308537 Binary files /dev/null and b/.reference_files/WhatsApp Image 2026-07-21 at 8.23.10 PM (2).jpeg differ diff --git a/.reference_files/WhatsApp Image 2026-07-21 at 8.23.10 PM.jpeg b/.reference_files/WhatsApp Image 2026-07-21 at 8.23.10 PM.jpeg new file mode 100644 index 0000000..c5a3315 Binary files /dev/null and b/.reference_files/WhatsApp Image 2026-07-21 at 8.23.10 PM.jpeg differ diff --git a/.reference_files/WhatsApp Image 2026-07-21 at 8.23.11 PM.jpeg b/.reference_files/WhatsApp Image 2026-07-21 at 8.23.11 PM.jpeg new file mode 100644 index 0000000..e852a7a Binary files /dev/null and b/.reference_files/WhatsApp Image 2026-07-21 at 8.23.11 PM.jpeg differ diff --git a/.reference_files/carlos-arias-positioning.md b/.reference_files/carlos-arias-positioning.md new file mode 100644 index 0000000..4efeb07 --- /dev/null +++ b/.reference_files/carlos-arias-positioning.md @@ -0,0 +1,163 @@ +# Carlos Arias — Repositioning Kit + +**From:** Digital Marketing Expert & Engineer +**To:** Agentic AI & Automation Engineer + +--- + +## Positioning statement (the one line that decides everything) + +**Carlos Arias** +**Agentic AI & Automation Engineer** +*Building autonomous software, AI agents, and intelligent business systems.* + +> Not an ML researcher who trains foundation models. An engineer who builds AI-powered products, agents, and autonomous workflows that replace work teams used to do by hand. + +**Tagline options** (pick one, use everywhere — site, LinkedIn, email signature): + +1. *Building autonomous software, AI agents & intelligent business systems.* +2. *I build software that builds, improves, and manages software.* +3. *Autonomous systems that do the work teams used to do by hand.* +4. *Agentic AI for founders and CTOs who want automation, not another dashboard.* + +--- + +## Hero section + +# Carlos Arias +### Agentic AI & Automation Engineer + +I design and build autonomous software, AI agents, and intelligent business systems that automate the complex workflows companies used to hand to whole teams. + +Over 26 years in software engineering and product development, building scalable platforms across FinTech, healthcare, and SaaS. Today my focus is singular: **Agentic AI — autonomous websites, business automation, and AI systems that continuously improve, adapt, and reduce manual work.** + +My work sits where software engineering, AI orchestration, automation, and product strategy meet — and I build the systems that live there. + +**[ Let's build your autonomous system → ]** + +--- + +## About + +I've been writing software since I was eight years old, on a Commodore 64 in 1988. Nearly four decades later, the instinct is the same: if a process is repetitive, it should build and run itself. + +For the last 26 years I've worked as a Software Engineer, CTO, Product Engineer, and Technology Advisor across startups and enterprise organizations — leading engineering teams and shipping platforms in FinTech, healthcare, SaaS, digital marketing, and geospatial technology. + +Since 2022 I've focused almost exclusively on **Agentic AI, autonomous software, and intelligent automation.** + +I don't just bolt AI onto existing applications. I design systems where multiple AI agents collaborate — researching, planning, deciding, generating content, writing software, running QA, and optimizing continuously, with minimal human intervention. + +The goal is simple: + +> **Build software that builds, improves, and manages software.** + +--- + +## Expertise + +*(Capabilities, not services — this is what signals "builder," not "agency.")* + +**Agentic AI Systems** +Multi-agent architectures where AI agents collaborate to solve complex business problems end to end. + +**AI Automation** +Replacing repetitive business processes with intelligent, autonomous workflows. + +**Autonomous Websites** +Sites that continuously improve their own SEO, content, UX, and conversions using AI. + +**AI Product Engineering** +AI-first SaaS platforms designed and shipped from concept to production. + +**Intelligent Marketing Systems** +AI that runs technical SEO, content planning, local SEO, competitor research, analytics, and optimization on its own. + +**Software Architecture** +APIs, cloud architecture, automation pipelines, distributed systems, and scalable backends. + +**Fractional CTO** +Technology leadership for startups building AI products. + +--- + +## Current Projects + +**Agentic Marketing Platform** — *In development* +An autonomous digital marketing platform where collaborative AI agents perform technical SEO, competitor research, content planning, local SEO, analytics, website optimization, and reporting — the full digital marketing process, run by agents. + +**Medellin.co** — *2020–present* +An AI-powered city platform using RAG, intelligent search, autonomous content generation, and AI assistants for travelers, expats, and local businesses. + +**Snoopi.io** — *2014–present* +A geolocation SaaS platform delivering IP intelligence, APIs, and location services for developers and enterprise applications. + +**Crawllr** — *2025–present* +An intelligent web crawler built for large-scale data collection, knowledge extraction, and AI indexing. + +**Gabii** — *In development* +An AI-powered relationship platform using intelligent profiling, compatibility analysis, and autonomous matchmaking. + +--- + +## Research & Concepts + +- Agentic Websites +- Autonomous SEO Systems +- Multi-Agent Collaboration +- AI Product Architecture +- Retrieval-Augmented Generation (RAG) +- AI Knowledge Systems +- AI Workflow Orchestration +- AI-Driven Software Development +- Self-Healing Applications +- Autonomous Business Systems + +--- + +## Industries + +Artificial Intelligence · SaaS · FinTech · Healthcare · Legal Technology · Digital Marketing · Location Intelligence · Automation Platforms + +--- + +## LinkedIn + +**Headline:** +Agentic AI & Automation Engineer | Building autonomous software, AI agents & intelligent business systems | Fractional CTO + +**About section:** +I build autonomous software — AI agents and intelligent systems that automate the complex workflows companies used to hand to whole teams. + +I've been writing code since I was eight, on a Commodore 64 in 1988. Across 26 years as a Software Engineer, CTO, and Technology Advisor, I've shipped platforms in FinTech, healthcare, SaaS, and geospatial tech. Since 2022 my focus has been singular: Agentic AI and intelligent automation. + +Rather than bolting AI onto existing apps, I design multi-agent systems that research, plan, decide, generate, build, and optimize on their own. Currently building an autonomous marketing platform, an AI-powered city platform (Medellin.co), and a geolocation SaaS (Snoopi.io). + +If you're a founder or CTO who wants automation that actually runs itself — not another dashboard — let's talk. + +--- + +## Short bios (grab-and-go) + +**One-liner:** +Carlos Arias is an Agentic AI & Automation Engineer who builds autonomous software, AI agents, and intelligent business systems. + +**50 words:** +Carlos Arias is an Agentic AI & Automation Engineer. With 26 years in software engineering and product development, he designs multi-agent systems that automate complex workflows — from autonomous websites to AI-driven marketing platforms. His focus since 2022: building software that builds, improves, and manages software with minimal human intervention. + +**100 words:** +Carlos Arias is an Agentic AI & Automation Engineer who builds autonomous software, AI agents, and intelligent business systems. He's been coding since age eight, on a Commodore 64 in 1988, and has spent 26 years as a Software Engineer, CTO, and Technology Advisor across FinTech, healthcare, SaaS, and geospatial technology. Since 2022 he's focused entirely on Agentic AI — designing multi-agent systems that research, plan, build, and optimize on their own. He's currently building an autonomous marketing platform, an AI-powered city platform (Medellin.co), and a geolocation SaaS (Snoopi.io). His mission: build software that builds, improves, and manages software. + +--- + +## What I cut (and why) + +Removed from the old positioning — these read as "digital agency owner," not "AI infrastructure builder": + +- Graphic Design +- Social Media management +- WordPress Developer +- Branding +- Website Design (as a standalone service) +- Digital Marketing *as a headline service* — now reframed as a **domain your AI systems automate**, not something you sell by hand. + +**One note to double-check:** the FinTech / security & compliance credentials (HIPAA, SOC2, PCI) from your original draft aren't in this version. They're a genuine trust signal for enterprise buyers — worth keeping as a single line under About or Expertise if the work backs it up. Say the word and I'll fold it in. diff --git a/.user.ini b/.user.ini new file mode 100644 index 0000000..d411df0 --- /dev/null +++ b/.user.ini @@ -0,0 +1 @@ +open_basedir=/www/wwwroot/CarlosAriasPersonal/:/tmp/ \ No newline at end of file diff --git a/.well-known/acme-challenge/eXf4NRu-MxHC0Dis4sHu5pC3gDElq1Y9p9LB0CSdHS0 b/.well-known/acme-challenge/eXf4NRu-MxHC0Dis4sHu5pC3gDElq1Y9p9LB0CSdHS0 new file mode 100644 index 0000000..0aab1bb --- /dev/null +++ b/.well-known/acme-challenge/eXf4NRu-MxHC0Dis4sHu5pC3gDElq1Y9p9LB0CSdHS0 @@ -0,0 +1 @@ +eXf4NRu-MxHC0Dis4sHu5pC3gDElq1Y9p9LB0CSdHS0.yunwhBDUlOIssanN_n7Md4A3OXu6B-DP-N3ietCHx8E \ No newline at end of file diff --git a/404.html b/404.html new file mode 100644 index 0000000..a2f560c --- /dev/null +++ b/404.html @@ -0,0 +1,74 @@ + + + + + + 404 Not Found + + + +
+ +
404
+
Sorry, the page you visited does not exist.
It may be that the access link is wrong or the file does not exist.
+ +
+ + diff --git a/502.html b/502.html new file mode 100644 index 0000000..5f25659 --- /dev/null +++ b/502.html @@ -0,0 +1,74 @@ + + + + + + 502 Bad Gateway + + + +
+ +
502
+
Gateway error.
The website server has timed out in returning data, please wait a few minutes and try again.
+ +
+ + diff --git a/agents/config.json b/agents/config.json index c272ddd..f83cbfc 100644 --- a/agents/config.json +++ b/agents/config.json @@ -1,13 +1,13 @@ { "site": { - "url": "https://example.com", - "name": "SeedProject Site", - "topic": "your subject area", - "audience": "your target audience", + "url": "https://carlosarias.co", + "name": "Carlos Arias", + "topic": "agentic AI, autonomous software, and business process automation", + "audience": "founders and CTOs who want automation, not another dashboard", "language": "en" }, "paths": { - "projectRoot": "/www/wwwroot/seedproject-web", + "projectRoot": "/www/wwwroot/CarlosAriasPersonal", "appDir": "app", "blogContentDir": "app/src/content/blog", "blogDataFile": "app/src/lib/blog-data.js", @@ -38,8 +38,8 @@ "maxPerScan": 8 }, "author": { - "slug": "site-author", - "name": "Site Author" + "slug": "carlos-arias", + "name": "Carlos Arias" }, "image": { "provider": "higgsfield", @@ -48,7 +48,7 @@ "outputFormat": "jpg", "credit": { "caption": "Illustrative cover image. Not a photograph of any specific establishment.", - "author": "SeedProject Site" + "author": "Carlos Arias" } }, "editorial": { diff --git a/agents/console/PLAN.md b/agents/console/PLAN.md new file mode 100644 index 0000000..13bc739 --- /dev/null +++ b/agents/console/PLAN.md @@ -0,0 +1,140 @@ +# Admin Agent — Plan + +**Goal:** manage the site through an AI agent (headless Claude) — create pages, +edit content, upload images — from two surfaces: a full **/admin dashboard** and +an **in-page console** ("open a page, tell it what to change"). + +**Decisions locked (2026-07-23):** +- **Full-builder agent** — can create pages, edit any file, restructure. Highest + ceiling, highest risk; sandboxing is mandatory, not optional. +- **Both surfaces** — /admin dashboard + in-page console. +- **Long-lived token** — `claude setup-token` on the server; agent reuses it. + +--- + +## What already exists (don't rebuild) + +- `app/src/components/DevConsole.astro` — the in-page console UI (prompt box, + element picker, preview link, Publish). Already on every live page, dormant. +- `astroagent.config.json` — engine config: model, tools + (`Read Write Edit Glob Grep WebSearch`), confined `agentUser` + (`carlos-arias-agent`), skills (`brand`, `ui-ux`, `seo`), console route/port. +- `api/` auth — `account.php` sessions + `ADMIN_TOKEN`. +- Structured content already DB-driven and agent-friendly: `cja_projects`, + `cja_changelog`, `cja_contact`, plus the seed scripts that write them. +- Server has the `claude` CLI and Node 22. Project is a git repo on `main`. + +## What's missing (the build) + +- The **runner service** the console talks to (`/devconsole/run|publish|...`) — + frontend exists, backend does not. +- **Image upload** anywhere in the flow. +- **Model selector** and the structured **draft + prompt + images** panel. +- The **/admin dashboard**. +- **Media pipeline** (`cja_media` + optimisation). +- **Hardening** — the code-writing agent is the highest-risk surface on the site. + +--- + +## Architecture + +``` +Browser (you, authed) + ├── /admin ................ dashboard SPA: pages, media, agent chat + └── in-page console ........ DevConsole.astro on any page + │ (same-origin fetch, session-authed) + ▼ + nginx ── /devconsole/* , /admin/* ──► localhost-only runner (Node, :3011) + │ auth gate on every request │ + │ ├── headless Claude Code + │ │ (agentUser, confined to repo, + │ │ tools: Read/Write/Edit/Glob/Grep/WebSearch) + │ ├── preview build → public-preview// + │ ├── publish → build public/ + chown + git commit + │ └── media: optimise → app/public/media/ + cja_media + ▼ + Static site (public/) — unchanged for visitors +``` + +**Content model — hybrid:** +- Freeform pages (`about`, `services/*`) → the agent edits `.astro` files directly. +- New pages → the agent writes new `.astro` files. +- Structured content (projects, changelog, services data) → DB, edited via + structured tools or the seed JSON/scripts. +- Images → `app/public/media/` (survives rebuilds) + `cja_media` rows. + +**Why preview-first matters:** the site is static. Every agent run builds to an +isolated `public-preview//` you can open and review; **nothing goes live +until you press Publish**, which rebuilds `public/` and commits to git. + +--- + +## Phases + +### Phase 0 — Foundation & safety *(do first, no features yet)* +- Commit the current working tree as a clean baseline (75 files) so every future + publish is a diff you can roll back. +- Create/confirm the confined `carlos-arias-agent` OS user: can write only the + repo, no sudo, no interactive shell. Install/point `claude` for that user. +- `claude setup-token` on the server → store in `agents/.env` (git-ignored). +- Admin auth: session login (reuse `api/account.php` or a dedicated admin login). +- nginx: `/devconsole/*` and `/admin/*` → localhost runner, **auth-gated**, never + exposed directly. Runner binds `127.0.0.1` only. +- Audit log table (`cja_admin_log`): who ran what, when, published or discarded. + +### Phase 1 — In-page console (finish the astroagent) +- Build the runner service: `run` (spawn headless Claude with prompt + page + context, stream output), `preview`, `publish`, `discard`, `ping`, `auth`, + `logout`. Conversation state so follow-ups work ("now make it bigger"). +- Wire `DevConsole.astro` to it. Element picker already works. +- Publish pipeline: build → verify build passes → chown → git commit. +- Milestone: open /about, "tighten the opening paragraph," preview, publish. + +### Phase 2 — Media +- `cja_media` table + `POST /devconsole/upload`. +- Optimise on upload (resize/compress, like the manual ffmpeg step), write to + `app/public/media/`, return URL + agent-generated alt text. +- Image upload in the console; agent can place uploaded images in a page. +- Milestone: on a project page, "add these three screenshots to the gallery." + +### Phase 3 — Admin dashboard (/admin) +- Login → dashboard. +- **Pages**: list (filesystem scan of `app/src/pages` + DB content types), + create-page flow ("make a page for my accounting services"), edit (hands off + to the console or a structured form). +- **Media library**: browse/upload/reuse. +- **Agent panel — your exact spec**: model selector · draft-content box · prompt + box · image-upload area. Draft + prompt + images post to the same runner. +- **Activity log** from `cja_admin_log`. + +### Phase 4 — Guardrails & polish +- Feed the agent the brand guide + `brand`/`ui-ux`/`seo` skills so edits stay + on-brand automatically. +- "Build passes" gate is required before Publish is offered. +- One-click **rollback** (git revert the last publish). +- Per-session run/token limits; rate limiting. + +--- + +## Risks & mitigations + +1. **Security is the whole game.** A code-writing agent on a public server is the + highest-value target here — a breach means full server control. Mitigations: + admin auth on every request, runner bound to localhost, confined OS user, + audit log, no direct exposure of the agent, rate limits. +2. **The agent can break the build.** → preview-first, build-passes gate, git + commit per publish, one-click rollback. +3. **Unbounded token spend.** → per-session limits; the long-lived token is one + account we can watch. +4. **On-brand drift.** → brand guide + skills injected into every agent run; + preview-before-publish is the human gate. + +## Not in scope (yet) +- Multi-user / roles (single admin for now). +- Scheduled autonomous edits (the `agents/` content pipeline is separate and + currently tuned for the old blog — re-tune later if wanted). + +## To start Phase 0, I need +- Your go-ahead to commit the current tree as the baseline. +- A decision on admin login: reuse the existing account system, or a simple + single-user admin password to start. diff --git a/api/app/Controllers/Contact.php b/api/app/Controllers/Contact.php new file mode 100644 index 0000000..e694a1a --- /dev/null +++ b/api/app/Controllers/Contact.php @@ -0,0 +1,111 @@ + 'AI Training', + 'workflow-automation' => 'Workflow Automation', + 'software-development' => 'Software Development', + 'website-design' => 'Website Design', + 'fractional-cto' => 'Fractional CTO', + 'other' => 'Other', + ]; + + public function submit(): void + { + // 5 submissions per 10 minutes per IP. + $this->guardPublic('contact_submit', 5, 600); + + if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { + $this->json(null, 405, ['code' => 'method_not_allowed', 'message' => 'POST only']); + } + + $in = $this->readBody(); + + // Honeypot: a field hidden from humans. A bot fills everything, so any + // value here is spam. Answer 200 so the bot cannot tell it was caught, + // but write nothing. + if (trim((string) ($in['company'] ?? '')) !== '') { + $this->json(['received' => true]); + } + + $name = trim((string) ($in['name'] ?? '')); + $email = trim((string) ($in['email'] ?? '')); + $subject = trim((string) ($in['subject'] ?? 'other')); + $message = trim((string) ($in['message'] ?? '')); + + $errors = []; + if ($name === '' || mb_strlen($name) > 160) { + $errors['name'] = 'Please enter your name.'; + } + if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL) || mb_strlen($email) > 255) { + $errors['email'] = 'Please enter a valid email address.'; + } + if (!isset(self::SUBJECTS[$subject])) { + // Unknown subject is coerced rather than rejected — never trust the + // client's list, but do not fail a real person over it either. + $subject = 'other'; + } + if (mb_strlen($message) < 10) { + $errors['message'] = 'Please add a little more detail (10 characters or more).'; + } + if (mb_strlen($message) > 5000) { + $errors['message'] = 'That message is a bit long — please keep it under 5000 characters.'; + } + + if ($errors) { + $this->json(null, 422, [ + 'code' => 'validation_failed', + 'message' => 'Please check the highlighted fields.', + 'fields' => $errors, + ]); + } + + \Db::insert('cja_contact', [ + 'name' => $name, + 'email' => $email, + 'subject' => $subject, + 'message' => $message, + 'status' => 'new', + 'ip' => $_SERVER['REMOTE_ADDR'] ?? null, + 'user_agent' => mb_substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255), + 'referrer' => mb_substr((string) ($_SERVER['HTTP_REFERER'] ?? ''), 0, 255), + 'created_at' => date('Y-m-d H:i:s'), + ]); + + // TODO: notify. A mailer helper exists (app/Helpers/Mailer.php, Brevo); + // wire it once a from-address and API key are configured. The row is + // saved either way, so a mail outage never loses a lead. + + $this->json([ + 'received' => true, + 'message' => "Thanks — I'll get back to you shortly.", + ]); + } + + /** Read a JSON body, falling back to form-encoded POST. */ + private function readBody(): array + { + $raw = file_get_contents('php://input'); + if ($raw !== '' && $raw !== false) { + $decoded = json_decode($raw, true); + if (is_array($decoded)) { + return $decoded; + } + } + return $_POST ?: []; + } +} diff --git a/api/cli/seed-changelog.php b/api/cli/seed-changelog.php new file mode 100644 index 0000000..35257e8 --- /dev/null +++ b/api/cli/seed-changelog.php @@ -0,0 +1,63 @@ + $type, + 'summary' => $summary, + 'detail' => $detail, + 'entry_at' => $at . ':00', + 'published' => 1, + ]); + $n++; +} + +echo " seeded {$n} changelog entries\n"; diff --git a/api/cli/seed-projects.php b/api/cli/seed-projects.php new file mode 100644 index 0000000..5bdb906 --- /dev/null +++ b/api/cli/seed-projects.php @@ -0,0 +1,373 @@ + PDO::ERRMODE_EXCEPTION] +); + +$projects = [ + [ + 'slug' => 'agentic-marketing-platform', + 'title' => 'Agentic Marketing Platform', + 'kind' => 'SaaS', + 'period_label' => 'In development', + 'started_on' => '2025-01-01', + 'ended_on' => null, + 'status' => 'building', + 'summary' => 'An autonomous digital marketing platform where collaborative agents run technical SEO, competitor research, content planning, local SEO, analytics, and reporting — the full process, run by agents.', + 'lede' => 'Most marketing tools give you a dashboard and leave the work to you. This one does the work.', + 'role' => 'Solo — architecture, agents, product', + 'categories' => ['Agentic', 'SaaS', 'Tools'], + 'stack' => ['Astro', 'PHP', 'MariaDB', 'LLM orchestration', 'MCP'], + 'skills' => [ + ['group' => 'Engineering', 'items' => ['Multi-agent architecture', 'Astro + PHP', 'MCP tooling']], + ['group' => 'AI', 'items' => ['LLM orchestration', 'Agent handoffs', 'Autonomous QA']], + ['group' => 'Marketing', 'items' => ['Technical SEO', 'Competitor research', 'Content planning']], + ], + 'metrics' => [ + ['label' => 'Agents', 'value' => '8', 'note' => 'research, plan, write, review, publish'], + ['label' => 'Human steps', 'value' => '1', 'note' => 'approve, or let it run'], + ], + 'links' => [], + 'body' => <<<'MD' +## The problem + +Digital marketing is a pipeline of repetitive judgement calls. Audit the site, +find the gaps, research what competitors rank for, plan the content, write it, +review it, publish it, measure it, and start again. Every step is +well-understood. Almost none of it is automated, because each step needs +context from the one before it. + +Tools solve this by giving you a dashboard. You still do the work. + +## The approach + +A set of specialist agents, each with a narrow job and a defined handoff, that +pass context down the pipeline rather than dumping it into one prompt. + +Research agents build the picture. Planning agents decide what to make. +Writing agents draft against a brief. Review agents check the draft against the +brief rather than against taste. A publishing agent ships it. + +The interesting problem is not any single agent — it is the handoff. Most +multi-agent systems fail because step four has forgotten why step two made the +decision it made. + +## Where it is + +In development. Running against live properties, not test data. +MD, + ], + [ + 'slug' => 'medellin-co', + 'title' => 'Medellin.co', + 'kind' => 'SaaS / Media', + 'period_label' => '2020 — present', + 'started_on' => '2022-10-01', + 'ended_on' => null, + 'status' => 'live', + 'summary' => 'An AI-powered city platform using RAG, intelligent search, autonomous content generation, and AI assistants for travelers, expats, and local businesses.', + 'lede' => 'The largest resource hub for Medellín, Colombia — built because the information simply did not exist in one place.', + 'role' => 'Founder — product, engineering, SEO, partnerships', + 'categories' => ['Websites', 'SaaS', 'Agentic'], + 'stack' => ['WordPress', 'Custom PHP', 'OpenAI', 'Zilliz', 'Stripe'], + 'skills' => [ + ['group' => 'Engineering', 'items' => ['Custom PHP', 'Third-party integrations', 'Payment systems']], + ['group' => 'AI', 'items' => ['RAG', 'Vector search', 'Embeddings', 'Chatbot design']], + ['group' => 'Marketing', 'items' => ['Technical SEO', 'Content strategy', 'Link building']], + ['group' => 'Leadership', 'items' => ['Managing an overseas team', 'Partnerships & advertising sales']], + ], + 'metrics' => [ + ['label' => 'Pages ranked', 'value' => '200+', 'note' => 'roughly 70% on page one'], + ['label' => 'Domain authority', 'value' => '10 → 30', 'note' => 'over three years'], + ], + // Placeholders — set src to a real path once photos exist. Empty src + // renders a labelled frame so the gallery layout is visible either way. + 'gallery' => [ + ['src' => '', 'alt' => 'Medellin.co homepage', 'caption' => 'Homepage'], + ['src' => '', 'alt' => 'AI assistant interface', 'caption' => 'AI assistant'], + ['src' => '', 'alt' => 'Business listing page', 'caption' => 'Business listings'], + ['src' => '', 'alt' => 'Advertiser dashboard', 'caption' => 'Advertiser dashboard'], + ], + 'links' => [ + 'live' => 'https://medellin.co', + 'instagram' => 'https://instagram.com/medellin.co', + ], + 'body' => <<<'MD' +## The problem + +Medellín has a large and growing population of tourists and expats, and almost +no reliable central resource written for them. Information was scattered across +outdated blogs, closed Facebook groups, and word of mouth. + +The domain had recently been released by the city. I negotiated with the owner +and acquired it in October 2022. + +## What it does + +A content platform with an AI layer on top. Visitors ask questions in natural +language and get answers grounded in the site's own content rather than a +general-purpose model's guesses — retrieval-augmented generation over an +embedded corpus of everything the site knows. + +Beyond content, it integrates directly with local systems: a path for users to +file criminal reports with law enforcement, and Stripe checkout for advertisers. + +## Engineering notes + +- Chatbot on an OpenAI LLM with a Zilliz vector database and OpenAI embeddings +- Custom PHP integrating directly with TikTok and Instagram +- Custom WordPress build using Elementor and Crocoblock +- Overseas team managed on design and development + +## Result + +The largest resource hub for the city, used by locals, expats, and tourists +before they arrive. 200+ pages optimised, roughly 70% ranking on page one, and +domain authority grown from 10 to 30. +MD, + ], + [ + 'slug' => 'verificaa', + 'title' => 'Verificaa.com', + 'kind' => 'SaaS', + 'period_label' => '2021 — present', + 'started_on' => '2021-06-01', + 'ended_on' => null, + 'status' => 'live', + 'summary' => 'ID verification and fraud prevention for Latin America — letting people verify businesses and individuals before doing business with them.', + 'lede' => 'Built because fraud in Latin America is common, and foreigners had no way to check who they were dealing with.', + 'role' => 'Founder — architecture, data pipeline, AI', + 'categories' => ['SaaS', 'Tools', 'Agentic'], + 'stack' => ['PHP', 'REST API', 'LLM', 'Data mining', 'Stripe'], + 'skills' => [ + ['group' => 'Engineering', 'items' => ['REST API design', 'API key management', 'Custom PHP dashboard']], + ['group' => 'Data', 'items' => ['Data mining', 'Crawling', 'Normalisation pipelines']], + ['group' => 'AI', 'items' => ['LLM fraud detection', 'Pattern analysis']], + ], + 'metrics' => [ + ['label' => 'Lives saved', 'value' => '2', 'note' => 'reported by users'], + ['label' => 'Sources', 'value' => 'Police, AG, business registries'], + ], + 'links' => ['live' => 'https://verificaa.com'], + 'body' => <<<'MD' +## The problem + +Fraud is common across Latin America, and foreigners doing business there have +almost no way to check who they are dealing with. The data exists — police +records, attorney general filings, business registries — but it is scattered, +inconsistently formatted, and effectively unsearchable. + +## What it does + +Verificaa normalises that data and puts a single lookup in front of it. Enter a +person or a business, get back what the public record actually says. + +An AI layer sits on top to flag patterns a human would miss: identities that +appear across unrelated filings, businesses registered days before a +transaction, documents that do not reconcile. + +## Engineering notes + +- Data-mining pipeline that crawls open Colombian and LATAM data and normalises it +- Integrations with police, attorney general, and business databases +- REST API with key management for developer access +- Custom PHP dashboard with a complex user management system +- LLM-driven fraud detection infrastructure + +## Result + +Users have reported the platform helping in two cases where someone's safety was +at risk, alongside a steady stream of prevented financial loss. +MD, + ], + [ + 'slug' => 'snoopi-io', + 'title' => 'Snoopi.io', + 'kind' => 'SaaS', + 'period_label' => '2015 — present', + 'started_on' => '2015-03-01', + 'ended_on' => null, + 'status' => 'live', + 'summary' => 'A geolocation SaaS platform delivering IP intelligence, APIs, and location services for developers and enterprise applications.', + 'lede' => 'Started as a way to learn PHP OOP properly. Ten years later it is still running, and still shipping.', + 'role' => 'Founder — everything', + 'categories' => ['SaaS', 'Tools'], + 'stack' => ['PHP', 'REST API', 'WordPress', 'Stripe'], + 'skills' => [ + ['group' => 'Engineering', 'items' => ['PHP OOP', 'REST API design', 'Real-time tracking']], + ['group' => 'Product', 'items' => ['Subscription billing', 'Invoice management', 'Custom dashboard']], + ['group' => 'Marketing', 'items' => ['GeoIP landing-page optimisation']], + ], + 'metrics' => [ + ['label' => 'Running since', 'value' => '2015', 'note' => 'ten years in production'], + ], + 'links' => ['live' => 'https://snoopi.io'], + 'body' => <<<'MD' +## Origin + +This one started as a learning project in 2015. I wanted to understand PHP +object-oriented programming properly, and building a real REST API that other +developers could actually use seemed like a better teacher than a tutorial. + +Ten years later it is still in production. + +## What it does + +GeoIP location and IP fraud prevention. Developers hit the API to resolve an IP +to a location; marketers use the tracking script to understand where landing +page traffic actually comes from. + +## Engineering notes + +- Real-time tracking script for sites and landing pages +- GeoIP-driven landing page optimisation +- Custom dashboard with invoice and subscription management +- Stripe integration using a custom card form + +## What is next + +AI analysis, to help marketers identify their best-performing pages rather than +just reporting where visitors came from. +MD, + ], + [ + 'slug' => 'crawllr', + 'title' => 'Crawllr', + 'kind' => 'Tooling', + 'period_label' => '2025 — present', + 'started_on' => '2025-01-01', + 'ended_on' => null, + 'status' => 'live', + 'summary' => 'An intelligent web crawler built for large-scale data collection, knowledge extraction, and AI indexing.', + 'lede' => 'Crawling is a solved problem. Crawling in a way an LLM can actually use is not.', + 'role' => 'Solo — architecture and build', + 'categories' => ['Tools', 'Agentic'], + 'stack' => ['PHP', 'MCP', 'LLM', 'Vector storage'], + 'skills' => [ + ['group' => 'Engineering', 'items' => ['Crawl architecture', 'MCP integration', 'Vector storage']], + ['group' => 'AI', 'items' => ['Knowledge extraction', 'Content chunking', 'Retrieval indexing']], + ], + 'metrics' => [], + 'links' => [], + 'body' => <<<'MD' +## The problem + +Fetching pages is trivial. Turning a site into something a language model can +reason over is not. Boilerplate has to go, structure has to survive, and the +result has to be chunked in a way that keeps meaning intact across boundaries. + +Most crawlers optimise for coverage. This one optimises for what comes after. + +## What it does + +Large-scale collection with knowledge extraction built into the pipeline rather +than bolted on afterwards. Output is indexed for retrieval, not just archived. + +It is exposed to agents over MCP, so an agent can crawl and reason in one loop +instead of waiting on a separate ingestion step. + +## Where it is + +Running in production, feeding the other projects on this list. +MD, + ], + [ + 'slug' => 'gabii', + 'title' => 'Gabii', + 'kind' => 'SaaS', + 'period_label' => 'In development', + 'started_on' => '2025-06-01', + 'ended_on' => null, + 'status' => 'building', + 'summary' => 'An AI-powered relationship platform using intelligent profiling, compatibility analysis, and autonomous matchmaking.', + 'lede' => 'Matching is a ranking problem that most platforms solve with engagement metrics rather than compatibility.', + 'role' => 'Founder — product and engineering', + 'categories' => ['SaaS', 'Agentic'], + 'stack' => ['LLM', 'Embeddings', 'PHP'], + 'skills' => [ + ['group' => 'Engineering', 'items' => ['PHP', 'Embedding pipelines']], + ['group' => 'AI', 'items' => ['Intelligent profiling', 'Compatibility modelling', 'Autonomous matching']], + ], + 'metrics' => [], + 'links' => [], + 'body' => <<<'MD' +## The idea + +Most matching platforms optimise for engagement, because engagement is what +they are measured on. That produces a product that works best when it does not +quite work. + +Gabii treats compatibility as the objective rather than time-on-app. + +## Approach + +Intelligent profiling built from how people actually describe themselves rather +than from checkbox attributes, compatibility analysis over embeddings, and +autonomous matchmaking that improves as it observes outcomes. + +## Where it is + +In development. +MD, + ], +]; + +$sql = <<<'SQL' +INSERT INTO cja_projects + (slug, title, kind, period_label, started_on, ended_on, status, summary, lede, + body, role, categories, stack, skills, metrics, gallery, links, published, published_at, sort_order) +VALUES + (:slug, :title, :kind, :period_label, :started_on, :ended_on, :status, :summary, :lede, + :body, :role, :categories, :stack, :skills, :metrics, :gallery, :links, 1, NOW(), :sort_order) +ON DUPLICATE KEY UPDATE + title = VALUES(title), kind = VALUES(kind), period_label = VALUES(period_label), + started_on = VALUES(started_on), ended_on = VALUES(ended_on), status = VALUES(status), + summary = VALUES(summary), lede = VALUES(lede), body = VALUES(body), role = VALUES(role), + categories = VALUES(categories), stack = VALUES(stack), skills = VALUES(skills), metrics = VALUES(metrics), gallery = VALUES(gallery), + links = VALUES(links), sort_order = VALUES(sort_order) +SQL; + +$stmt = $pdo->prepare($sql); +$json = static fn($v) => json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + +foreach ($projects as $i => $p) { + $stmt->execute([ + ':slug' => $p['slug'], + ':title' => $p['title'], + ':kind' => $p['kind'], + ':period_label' => $p['period_label'], + ':started_on' => $p['started_on'], + ':ended_on' => $p['ended_on'], + ':status' => $p['status'], + ':summary' => $p['summary'], + ':lede' => $p['lede'], + ':body' => $p['body'], + ':role' => $p['role'], + ':categories' => $json($p['categories']), + ':stack' => $json($p['stack']), + ':skills' => $json($p['skills'] ?? []), + ':metrics' => $json($p['metrics']), + ':gallery' => $json($p['gallery'] ?? []), + ':links' => $json($p['links']), + ':sort_order' => $i * 10, + ]); + printf(" %-30s %s\n", $p['slug'], 'ok'); +} + +$n = $pdo->query('SELECT COUNT(*) FROM cja_projects')->fetchColumn(); +echo "\n {$n} projects in cja_projects\n"; diff --git a/api/db/migrations/003_schema_snapshot.sql b/api/db/migrations/003_schema_snapshot.sql index a488aa8..8bae915 100644 --- a/api/db/migrations/003_schema_snapshot.sql +++ b/api/db/migrations/003_schema_snapshot.sql @@ -3,6 +3,13 @@ -- every table uses CREATE TABLE IF NOT EXISTS, so it composes with the existing migrations. -- Generated 2026-07-11. +-- NOTE: this snapshot declares foreign keys before their referenced tables +-- exist (mde_comments -> sp_users appears ~1100 lines earlier than sp_users). +-- The export was generated without mysqldump's usual FK guard, so add it here. +-- Constraints are still validated on the next write; this only defers checking +-- during table creation, exactly as install/dump.sql already does. +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; + /*M!999999\- enable the sandbox mode */ /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -1313,3 +1320,4 @@ CREATE TABLE IF NOT EXISTS `sp_zipcode` ( PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; /*!40101 SET character_set_client = @saved_cs_client */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; diff --git a/api/db/migrations/004_create_cja_projects.sql b/api/db/migrations/004_create_cja_projects.sql new file mode 100644 index 0000000..7d66696 --- /dev/null +++ b/api/db/migrations/004_create_cja_projects.sql @@ -0,0 +1,67 @@ +-- Projects — the source of truth for /projects and /projects/{slug}. +-- +-- Two shapes live in one row: the short form the index needs (title, period, +-- summary, categories) and the long form a case-study page needs (lede, body, +-- metrics, links). Keeping them together avoids a join on every page render +-- and keeps editing in one place. +-- +-- JSON is used for the small repeating lists — categories, stack, metrics, +-- links. At this scale that is the right trade: a normalised category table +-- would add two tables and a join to support a filter over a handful of rows. +-- MariaDB can still query inside them via JSON_CONTAINS if that is ever needed. +-- If projects grow past a few dozen, or categories need their own descriptions +-- and pages, promote `categories` to cja_project_categories + a map table. + +CREATE TABLE IF NOT EXISTS `cja_projects` ( + `project_id` int(10) unsigned NOT NULL AUTO_INCREMENT, + + -- identity + `slug` varchar(120) NOT NULL, + `title` varchar(160) NOT NULL, + `kind` varchar(60) NOT NULL DEFAULT '', -- "SaaS", "SaaS / Media" + + -- timeline. period_label is what renders; the dates are what sorts, so an + -- ongoing project ("2020 — present") still orders correctly against a dated one. + `period_label` varchar(60) NOT NULL DEFAULT '', + `started_on` date DEFAULT NULL, + `ended_on` date DEFAULT NULL, -- NULL = ongoing + `status` enum('live','building','archived','concept') + NOT NULL DEFAULT 'live', + + -- copy + `summary` varchar(500) NOT NULL DEFAULT '', -- index row + `lede` text DEFAULT NULL, -- detail page opener + `body` mediumtext DEFAULT NULL, -- markdown + `role` varchar(200) NOT NULL DEFAULT '', -- "Solo — design, build, ops" + + -- structured lists + `categories` longtext DEFAULT NULL CHECK (json_valid(`categories`)), + `stack` longtext DEFAULT NULL CHECK (json_valid(`stack`)), + `metrics` longtext DEFAULT NULL CHECK (json_valid(`metrics`)), + `links` longtext DEFAULT NULL CHECK (json_valid(`links`)), + + -- media + `cover_image` varchar(255) NOT NULL DEFAULT '', + `cover_alt` varchar(255) NOT NULL DEFAULT '', + + -- seo. Nullable so the page can fall back to title/summary rather than + -- forcing an editor to duplicate them. + `seo_title` varchar(180) DEFAULT NULL, + `seo_description` varchar(320) DEFAULT NULL, + + -- publishing + `featured` tinyint(1) NOT NULL DEFAULT 0, + `sort_order` int(11) NOT NULL DEFAULT 0, + `published` tinyint(1) NOT NULL DEFAULT 0, + `published_at` datetime DEFAULT NULL, + + `created_at` datetime NOT NULL DEFAULT current_timestamp(), + `updated_at` datetime NOT NULL DEFAULT current_timestamp() + ON UPDATE current_timestamp(), + + PRIMARY KEY (`project_id`), + UNIQUE KEY `uq_cja_projects_slug` (`slug`), + KEY `idx_cja_projects_listing` (`published`, `sort_order`, `started_on`), + KEY `idx_cja_projects_status` (`status`), + KEY `idx_cja_projects_featured` (`featured`, `sort_order`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/api/db/migrations/005_create_cja_contact.sql b/api/db/migrations/005_create_cja_contact.sql new file mode 100644 index 0000000..b503347 --- /dev/null +++ b/api/db/migrations/005_create_cja_contact.sql @@ -0,0 +1,36 @@ +-- Contact form submissions. +-- +-- `subject` is stored as free text (a slug from a known list), not an ENUM. +-- ENUMs need a migration every time an option is added; the list of enquiry +-- types will change more often than the schema should. The controller +-- validates against the allowed set, so bad values never reach here. +-- +-- Anti-spam and audit columns live alongside the message: a honeypot outcome, +-- the origin IP, and the user agent. These make it possible to review what got +-- flagged without a separate log. + +CREATE TABLE IF NOT EXISTS `cja_contact` ( + `contact_id` int(10) unsigned NOT NULL AUTO_INCREMENT, + + `name` varchar(160) NOT NULL, + `email` varchar(255) NOT NULL, + `subject` varchar(60) NOT NULL DEFAULT 'other', + `message` text NOT NULL, + + -- lifecycle so the inbox is workable, not just a dump + `status` enum('new','read','replied','spam','archived') + NOT NULL DEFAULT 'new', + + -- audit / anti-spam + `ip` varchar(45) DEFAULT NULL, -- v4 or v6 + `user_agent` varchar(255) DEFAULT NULL, + `referrer` varchar(255) DEFAULT NULL, + + `created_at` datetime NOT NULL DEFAULT current_timestamp(), + `updated_at` datetime NOT NULL DEFAULT current_timestamp() + ON UPDATE current_timestamp(), + + PRIMARY KEY (`contact_id`), + KEY `idx_cja_contact_status` (`status`, `created_at`), + KEY `idx_cja_contact_email` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/api/db/migrations/006_projects_gallery_skills.sql b/api/db/migrations/006_projects_gallery_skills.sql new file mode 100644 index 0000000..d9ef69d --- /dev/null +++ b/api/db/migrations/006_projects_gallery_skills.sql @@ -0,0 +1,19 @@ +-- Richer project detail pages: a photo gallery and a grouped skills breakdown. +-- +-- Both are JSON, same reasoning as the categories/metrics columns already on +-- this table: small, per-row, repeating lists that never need to be queried +-- across projects. A gallery image belongs to exactly one project; normalising +-- it into cja_project_images would add a table and a join to store what is +-- effectively an ordered list. +-- +-- gallery : [ { "src": "/projects/x/1.jpg", "alt": "...", "caption": "..." }, ... ] +-- skills : [ { "group": "Engineering", "items": ["API design", "..."] }, ... ] +-- +-- `links` already exists and is JSON — no change needed. It now carries more +-- keys: { "live": "...", "instagram": "...", "github": "..." }. + +ALTER TABLE `cja_projects` + ADD COLUMN `gallery` longtext DEFAULT NULL + CHECK (json_valid(`gallery`)) AFTER `metrics`, + ADD COLUMN `skills` longtext DEFAULT NULL + CHECK (json_valid(`skills`)) AFTER `stack`; diff --git a/api/db/migrations/007_create_cja_changelog.sql b/api/db/migrations/007_create_cja_changelog.sql new file mode 100644 index 0000000..5cf516f --- /dev/null +++ b/api/db/migrations/007_create_cja_changelog.sql @@ -0,0 +1,26 @@ +-- Public, human-friendly changelog for the site itself. +-- +-- Fits the "agentic website" thesis: the site documents its own evolution, and +-- an agent can append a row here as part of shipping a change. Same JSON-free, +-- flat shape the rest of the cja_* tables use — this is a log, not a graph. +-- +-- `type` is a small closed set, so an ENUM is correct here (unlike the contact +-- subject, which grows). `entry_at` is the event time shown to readers; the row +-- timestamps are for bookkeeping. + +CREATE TABLE IF NOT EXISTS `cja_changelog` ( + `changelog_id` int(10) unsigned NOT NULL AUTO_INCREMENT, + + `type` enum('added','updated','fixed','bug','removed','note') + NOT NULL DEFAULT 'added', + `summary` varchar(255) NOT NULL, + `detail` varchar(500) DEFAULT NULL, + + `entry_at` datetime NOT NULL, -- when the change happened + `published` tinyint(1) NOT NULL DEFAULT 1, + + `created_at` datetime NOT NULL DEFAULT current_timestamp(), + + PRIMARY KEY (`changelog_id`), + KEY `idx_cja_changelog_feed` (`published`, `entry_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/api/public/controllers/contact.php b/api/public/controllers/contact.php new file mode 100644 index 0000000..68864ba --- /dev/null +++ b/api/public/controllers/contact.php @@ -0,0 +1,113 @@ + 'Automation', + 'website-design' => 'Website Design', + 'digital-marketing' => 'Digital Marketing', + 'fractional-cto' => 'Fractional CTO', + 'other' => 'Other', + ]; + + public function submit(): void + { + // 5 submissions per 10 minutes per IP — generous for a human, useless + // for a script. + $this->guardPublic('contact_submit', 5, 600); + + if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { + $this->json(null, 405, ['code' => 'method_not_allowed', 'message' => 'POST only']); + } + + $in = $this->readBody(); + + // Honeypot: a field hidden from humans. A bot fills everything, so any + // value here is spam. Answer 200 so it cannot tell it was caught, but + // write nothing. + if (trim((string) ($in['company'] ?? '')) !== '') { + $this->json(['received' => true]); + } + + $name = trim((string) ($in['name'] ?? '')); + $email = trim((string) ($in['email'] ?? '')); + $subject = trim((string) ($in['subject'] ?? 'other')); + $message = trim((string) ($in['message'] ?? '')); + + $errors = []; + if ($name === '' || mb_strlen($name) > 160) { + $errors['name'] = 'Please enter your name.'; + } + if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL) || mb_strlen($email) > 255) { + $errors['email'] = 'Please enter a valid email address.'; + } + if (!isset(self::SUBJECTS[$subject])) { + // Unknown subject is coerced, not rejected — never trust the + // client's list, but do not fail a real person over it either. + $subject = 'other'; + } + if (mb_strlen($message) < 10) { + $errors['message'] = 'Please add a little more detail (10 characters or more).'; + } + if (mb_strlen($message) > 5000) { + $errors['message'] = 'That message is a bit long — please keep it under 5000 characters.'; + } + + if ($errors) { + $this->json(null, 422, [ + 'code' => 'validation_failed', + 'message' => 'Please check the highlighted fields.', + 'fields' => $errors, + ]); + } + + \Db::insert('cja_contact', [ + 'name' => $name, + 'email' => $email, + 'subject' => $subject, + 'message' => $message, + 'status' => 'new', + 'ip' => $_SERVER['REMOTE_ADDR'] ?? null, + 'user_agent' => mb_substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255), + 'referrer' => mb_substr((string) ($_SERVER['HTTP_REFERER'] ?? ''), 0, 255), + 'created_at' => date('Y-m-d H:i:s'), + ]); + + // TODO: notify. A mailer helper exists (app/Helpers/Mailer.php, Brevo); + // wire it once a from-address and API key are configured. The row is + // saved regardless, so a mail outage never loses a lead. + + $this->json([ + 'received' => true, + 'message' => "Thanks — I'll get back to you shortly.", + ]); + } + + /** Read a JSON body, falling back to form-encoded POST. */ + private function readBody(): array + { + $raw = file_get_contents('php://input'); + if ($raw !== '' && $raw !== false) { + $decoded = json_decode($raw, true); + if (is_array($decoded)) { + return $decoded; + } + } + return $_POST ?: []; + } +} diff --git a/api/system/errors.json b/api/system/errors.json index fe51488..57e78f2 100644 --- a/api/system/errors.json +++ b/api/system/errors.json @@ -1 +1,542 @@ -[] +[ + { + "id": "err_6a6204ee4c9044.13450162", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/carlosarias.co\/api\/contact\/submit", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "POST", + "ip_address": "207.246.79.95", + "user_agent": "curl\/8.18.0", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 08:11:26" + }, + { + "id": "err_6a6204e4189018.64313489", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/carlosarias.co\/api\/contact\/submit", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "POST", + "ip_address": "207.246.79.95", + "user_agent": "curl\/8.18.0", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 08:11:16" + }, + { + "id": "err_6a6204e411d7f1.68494780", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/carlosarias.co\/api\/contact\/submit", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "POST", + "ip_address": "207.246.79.95", + "user_agent": "curl\/8.18.0", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 08:11:16" + }, + { + "id": "err_6a6204e40b68c3.54833855", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/carlosarias.co\/api\/contact\/submit", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "POST", + "ip_address": "207.246.79.95", + "user_agent": "curl\/8.18.0", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 08:11:16" + }, + { + "id": "err_6a61d83d740d58.03977982", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95\/api\/.env", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "45.148.10.200", + "user_agent": "l9explore\/1.2.2", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 05:00:45" + }, + { + "id": "err_6a61d585ba82c5.93490924", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95.vultrusercontent.com\/api\/v2.0\/systeminfo", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "45.79.102.191", + "user_agent": "Mozilla\/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit\/537.36 (KHTML", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 04:49:09" + }, + { + "id": "err_6a61d585ad8eb4.47467749", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95.vultrusercontent.com\/api\/v1\/cluster\/summary", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "45.79.102.191", + "user_agent": "Mozilla\/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit\/537.36 (KHTML", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 04:49:09" + }, + { + "id": "err_6a61d583f013a6.04024915", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95.vultrusercontent.com\/api\/status", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "45.79.102.191", + "user_agent": "Mozilla\/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit\/537.36 (KHTML", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 04:49:07" + }, + { + "id": "err_6a61d58384e463.42090894", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95.vultrusercontent.com\/api\/v1\/info", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "45.79.102.191", + "user_agent": "Mozilla\/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit\/537.36 (KHTML", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 04:49:07" + }, + { + "id": "err_6a61d583390081.28105900", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95.vultrusercontent.com\/api\/v2\/hoverfly\/version", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "45.79.102.191", + "user_agent": "Mozilla\/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit\/537.36 (KHTML", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 04:49:07" + }, + { + "id": "err_6a61d58180e553.92715360", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95.vultrusercontent.com\/api\/v1\/version", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "45.79.102.191", + "user_agent": "Mozilla\/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit\/537.36 (KHTML", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 04:49:05" + }, + { + "id": "err_6a61d5816e61e3.32538381", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95.vultrusercontent.com\/api\/v3\/meta", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "45.79.102.191", + "user_agent": "Mozilla\/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit\/537.36 (KHTML", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 04:49:05" + }, + { + "id": "err_6a61d580826025.08194969", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95.vultrusercontent.com\/api\/v1\/check-version", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "45.79.102.191", + "user_agent": "Mozilla\/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit\/537.36 (KHTML", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 04:49:04" + }, + { + "id": "err_6a61d57fa07a28.14708581", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95.vultrusercontent.com\/api\/v1.0\/environment", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "45.79.102.191", + "user_agent": "Mozilla\/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit\/537.36 (KHTML", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 04:49:03" + }, + { + "id": "err_6a61d57ed790d0.28757436", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95.vultrusercontent.com\/api\/vip\/i18n\/api\/v2\/translation\/products\/vRNIUI\/versions\/1", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "45.79.102.191", + "user_agent": "Mozilla\/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit\/537.36 (KHTML", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 04:49:02" + }, + { + "id": "err_6a61d57dded5b3.26360675", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95.vultrusercontent.com\/api\/version", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "45.79.102.191", + "user_agent": "Mozilla\/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit\/537.36 (KHTML", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 04:49:01" + }, + { + "id": "err_6a61d57be8d4c8.44732492", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95.vultrusercontent.com\/api\/v2\/about", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "45.79.102.191", + "user_agent": "Mozilla\/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit\/537.36 (KHTML", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-23 04:48:59" + }, + { + "id": "err_6a6131dbc671d4.14718320", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95:443\/api\/.env", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "80.94.95.211", + "user_agent": "Mozilla\/5.0 (X11; Linux x86_64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/61.0.3163.79 Safari\/537.36", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-22 17:10:51" + }, + { + "id": "err_6a6130caea4be3.10187418", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/app.medellin.co\/api\/graphql", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "POST", + "ip_address": "104.28.229.13", + "user_agent": "Go-http-client\/2.0", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-22 17:06:18" + }, + { + "id": "err_6a6130c94f7da9.47659370", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/app.medellin.co\/api\/.env", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "104.28.229.13", + "user_agent": "Mozilla\/5.0 AppleWebKit\/537.36 (KHTML, like Gecko); compatible; PerplexityBot\/1.0; +https:\/\/perplexity.ai\/perplexitybot", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-22 17:06:17" + }, + { + "id": "err_6a612f82634e08.62465813", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/app.medellin.co\/api\/.env", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "104.28.211.190", + "user_agent": "Mozilla\/5.0 (compatible; GoogleOther; +http:\/\/www.google.com\/bot.html)", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-22 17:00:50" + }, + { + "id": "err_6a612f81369191.96179634", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/app.medellin.co\/api\/graphql", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "POST", + "ip_address": "104.28.211.190", + "user_agent": "Go-http-client\/2.0", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-22 17:00:49" + }, + { + "id": "err_6a6113b87c9b70.54588185", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/207.246.79.95:443\/api\/admin\/token", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php", + "line": 222, + "fullerror": "Call to undefined method Admin::index()", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(64): Bootstrap->_callControllerMethod()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#3 {main}", + "method": "GET", + "ip_address": "45.144.28.70", + "user_agent": "Mozilla\/5.0", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-22 15:02:16" + }, + { + "id": "err_6a60c8cf7dfe21.13179858", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/www.carlosarias.co\/api\/.env", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "172.69.33.15", + "user_agent": null, + "referer": null, + "status": "unresolved", + "createdate": "2026-07-22 09:42:39" + }, + { + "id": "err_6a60bb222642f1.89719899", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/carlosarias.co\/api\/console", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "207.246.79.95", + "user_agent": "curl\/8.18.0", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-22 08:44:18" + }, + { + "id": "err_6a60bb221ea3e6.68499605", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/carlosarias.co\/api\/vendor\/autoload.php", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "207.246.79.95", + "user_agent": "curl\/8.18.0", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-22 08:44:18" + }, + { + "id": "err_6a60bb22156601.09261541", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/carlosarias.co\/api\/composer.json", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "207.246.79.95", + "user_agent": "curl\/8.18.0", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-22 08:44:18" + }, + { + "id": "err_6a60bb220de8d8.86707709", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/carlosarias.co\/api\/config.php", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "207.246.79.95", + "user_agent": "curl\/8.18.0", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-22 08:44:18" + }, + { + "id": "err_6a60bb22066702.98933056", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/carlosarias.co\/api\/install\/", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "207.246.79.95", + "user_agent": "curl\/8.18.0", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-22 08:44:18" + }, + { + "id": "err_6a60bb0d8f2a18.55698756", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/carlosarias.co\/api\/install\/", + "file": "\/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/controllers\/error.php", + "line": 19, + "fullerror": "Undefined constant \"COMPANY\"", + "trace": "#0 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(244): _Error->index()\n#1 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(151): Bootstrap->_error()\n#2 \/www\/wwwroot\/CarlosAriasPersonal\/api\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#3 \/www\/wwwroot\/CarlosAriasPersonal\/api\/public\/index.php(22): Bootstrap->init()\n#4 \/www\/wwwroot\/CarlosAriasPersonal\/api\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "207.246.79.95", + "user_agent": "curl\/8.18.0", + "referer": null, + "status": "unresolved", + "createdate": "2026-07-22 08:43:57" + } +] \ No newline at end of file diff --git a/app/app/package-lock.json b/app/app/package-lock.json new file mode 100644 index 0000000..716301b --- /dev/null +++ b/app/app/package-lock.json @@ -0,0 +1,127 @@ +{ + "name": "app", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^1.25.0", + "motion": "^12.42.2", + "tailwind-merge": "^3.6.0" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/framer-motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", + "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.42.2", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/lucide-react": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.25.0.tgz", + "integrity": "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.2.tgz", + "integrity": "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.42.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", + "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + } + } +} diff --git a/app/app/package.json b/app/app/package.json new file mode 100644 index 0000000..3d9bbf5 --- /dev/null +++ b/app/app/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^1.25.0", + "motion": "^12.42.2", + "tailwind-merge": "^3.6.0" + } +} diff --git a/app/astro.config.mjs b/app/astro.config.mjs index 8247250..00245f0 100644 --- a/app/astro.config.mjs +++ b/app/astro.config.mjs @@ -5,6 +5,7 @@ const siteConfig = JSON.parse( readFileSync(new URL("./src/config/site.json", import.meta.url)), ); import mdx from "@astrojs/mdx"; +import react from "@astrojs/react"; import tailwindcss from "@tailwindcss/vite"; const site = @@ -18,11 +19,21 @@ export default defineConfig({ // For previews, PREVIEW_BASE prefixes asset paths (/_preview/) so the // preview is self-contained and doesn't pull _astro/fonts from the live site. base: process.env.PREVIEW_BASE || undefined, - integrations: [mdx()], + integrations: [mdx(), react()], vite: { plugins: [tailwindcss()], + resolve: { + alias: { + // shadcn / 21st.dev convention — paste components in unmodified. + "@": new URL("./src", import.meta.url).pathname, + // Those components import useTheme from "next-themes", which is a + // Next.js package. Point it at a local shim that drives the existing + // class-based theme instead. See src/lib/next-themes.ts. + "next-themes": new URL("./src/lib/next-themes.ts", import.meta.url).pathname, + }, + }, build: { emptyOutDir: false, }, }, -}); +}); \ No newline at end of file diff --git a/app/components.json b/app/components.json new file mode 100644 index 0000000..671ce1f --- /dev/null +++ b/app/components.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "ui": "@/components/ui", + "utils": "@/lib/utils", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": { + "@21st": { + "url": "https://21st.dev/r/{name}", + "headers": { + "Authorization": "Bearer ${TWENTYFIRST_API_KEY}" + } + } + } +} diff --git a/app/package-lock.json b/app/package-lock.json index 75f57d2..cb8ec1c 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -10,12 +10,26 @@ "license": "MIT", "dependencies": { "@astrojs/mdx": "^6.0.3", + "@astrojs/react": "^6.0.1", + "@radix-ui/react-slot": "^1.3.0", "@tailwindcss/vite": "^4.3.1", "astro": "^6.4.7", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.42.2", + "lucide-react": "^1.25.0", + "marked": "^18.0.7", + "motion": "^12.42.2", + "mysql2": "^3.23.1", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.1", "tw-animate-css": "^1.4.0" }, "devDependencies": { + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", "prettier": "^3.8.4", "vite": "^7.3.5" } @@ -113,6 +127,121 @@ "node": ">=22.12.0" } }, + "node_modules/@astrojs/react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@astrojs/react/-/react-6.0.1.tgz", + "integrity": "sha512-Afs1sEm72P2plDnrOGxmIteJ7bjx/VqxlcaQLNip5eHJ5tIvKUORQetC9UKcvgwKnj51t60HWl5mOANkOsWs4w==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.10.1", + "@vitejs/plugin-react": "^5.2.0", + "devalue": "^5.8.1", + "ultrahtml": "^1.6.0", + "vite": "^8.0.13" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", + "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", + "react": "^17.0.2 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@astrojs/react/node_modules/@astrojs/internal-helpers": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.1.tgz", + "integrity": "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "js-yaml": "^4.1.1", + "picomatch": "^4.0.4", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "unified": "^11.0.5" + } + }, + "node_modules/@astrojs/react/node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/@astrojs/telemetry": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz", @@ -129,6 +258,166 @@ "node": "18.20.8 || ^20.3.0 || >=22.0.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -147,6 +436,28 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", @@ -162,6 +473,68 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", @@ -216,13 +589,13 @@ } }, "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, @@ -237,9 +610,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "license": "MIT", "optional": true, "dependencies": { @@ -1211,13 +1584,13 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.2" + "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", @@ -1234,6 +1607,296 @@ "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "license": "MIT" }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", + "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", + "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "license": "MIT" + }, "node_modules/@rollup/pluginutils": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", @@ -1945,15 +2608,56 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -2017,6 +2721,34 @@ "@types/unist": "*" } }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2029,6 +2761,26 @@ "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", "license": "ISC" }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -2187,6 +2939,15 @@ "sharp": "^0.34.0" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2206,12 +2967,77 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.0.tgz", + "integrity": "sha512-oCu2wfipvX3AePSgmOuKkIywOu+8n9psz7hXYmk56ghpu3+7KzNIBopaOs4c9BrtdnTtW30unG9GTfHo7EwERQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -2292,6 +3118,18 @@ "node": ">=8" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2339,6 +3177,12 @@ "node": ">= 18" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -2441,6 +3285,12 @@ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "license": "CC0-1.0" }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2477,6 +3327,15 @@ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "license": "MIT" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2605,6 +3464,12 @@ "node": ">=4" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.395", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz", + "integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==", + "license": "ISC" + }, "node_modules/enhanced-resolve": { "version": "5.21.6", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", @@ -2709,6 +3574,15 @@ "@esbuild/win32-x64": "0.27.7" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", @@ -2895,6 +3769,33 @@ "node": ">=20" } }, + "node_modules/framer-motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", + "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.42.2", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2909,6 +3810,24 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-tsconfig": { "version": "5.0.0-beta.4", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz", @@ -3207,6 +4126,22 @@ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "license": "BSD-2-Clause" }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", @@ -3326,6 +4261,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-wsl": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", @@ -3350,6 +4291,12 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", @@ -3372,6 +4319,30 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonc-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", @@ -3627,6 +4598,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -3646,6 +4623,30 @@ "node": "20 || >=22" } }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/lucide-react": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.25.0.tgz", + "integrity": "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -3688,6 +4689,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "18.0.7", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.7.tgz", + "integrity": "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/mdast-util-definitions": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", @@ -4713,6 +5726,47 @@ ], "license": "MIT" }, + "node_modules/motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.2.tgz", + "integrity": "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.42.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", + "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -4728,10 +5782,44 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.23.1.tgz", + "integrity": "sha512-tTuRnC7qCet2IOfSNMYZ5SwXuBnfvBPAcIA28P0gtruXyZlU1LMxA6uha32kYypoFgyYklMqhLWwt4laYwXR/Q==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.5.1" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -4780,6 +5868,15 @@ "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", "license": "MIT" }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4965,9 +6062,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -4977,9 +6074,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", "funding": [ { "type": "opencollective", @@ -4996,7 +6093,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5045,6 +6142,36 @@ "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", "license": "MIT" }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -5390,6 +6517,45 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.62.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", @@ -5434,6 +6600,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/sax": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", @@ -5443,6 +6615,12 @@ "node": ">=11.0.0" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", @@ -5565,6 +6743,21 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/sql-escaper": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.1.tgz", + "integrity": "sha512-4toX5E1fQbBrpfXidaHnF0669nkAdETeIPTs2SUjxxD7RRIs9ICG4gtpmfc68JCEKehsdwLFqBu9VlQqZ1P1gg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -5622,6 +6815,16 @@ "url": "https://opencollective.com/svgo" } }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", @@ -5705,8 +6908,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/tw-animate-css": { "version": "1.4.0", @@ -5735,6 +6937,13 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT", + "peer": true + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -5997,6 +7206,36 @@ } } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -6157,6 +7396,12 @@ "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", "license": "MIT" }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, "node_modules/yargs-parser": { "version": "22.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", diff --git a/app/package.json b/app/package.json index 2ae8453..beed14e 100644 --- a/app/package.json +++ b/app/package.json @@ -13,12 +13,26 @@ }, "dependencies": { "@astrojs/mdx": "^6.0.3", + "@astrojs/react": "^6.0.1", + "@radix-ui/react-slot": "^1.3.0", "@tailwindcss/vite": "^4.3.1", "astro": "^6.4.7", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.42.2", + "lucide-react": "^1.25.0", + "marked": "^18.0.7", + "motion": "^12.42.2", + "mysql2": "^3.23.1", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.1", "tw-animate-css": "^1.4.0" }, "devDependencies": { + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", "prettier": "^3.8.4", "vite": "^7.3.5" } diff --git a/app/public/.well-known/acme-challenge/eXf4NRu-MxHC0Dis4sHu5pC3gDElq1Y9p9LB0CSdHS0 b/app/public/.well-known/acme-challenge/eXf4NRu-MxHC0Dis4sHu5pC3gDElq1Y9p9LB0CSdHS0 new file mode 100644 index 0000000..0aab1bb --- /dev/null +++ b/app/public/.well-known/acme-challenge/eXf4NRu-MxHC0Dis4sHu5pC3gDElq1Y9p9LB0CSdHS0 @@ -0,0 +1 @@ +eXf4NRu-MxHC0Dis4sHu5pC3gDElq1Y9p9LB0CSdHS0.yunwhBDUlOIssanN_n7Md4A3OXu6B-DP-N3ietCHx8E \ No newline at end of file diff --git a/app/public/404-server.html b/app/public/404-server.html new file mode 100644 index 0000000..a2f560c --- /dev/null +++ b/app/public/404-server.html @@ -0,0 +1,74 @@ + + + + + + 404 Not Found + + + +
+ +
404
+
Sorry, the page you visited does not exist.
It may be that the access link is wrong or the file does not exist.
+ +
+ + diff --git a/app/public/404/stick0.svg b/app/public/404/stick0.svg new file mode 100644 index 0000000..f8f6274 --- /dev/null +++ b/app/public/404/stick0.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/public/404/stick1.svg b/app/public/404/stick1.svg new file mode 100644 index 0000000..63ec7d8 --- /dev/null +++ b/app/public/404/stick1.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/public/404/stick2.svg b/app/public/404/stick2.svg new file mode 100644 index 0000000..6b0e6b4 --- /dev/null +++ b/app/public/404/stick2.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/public/404/stick3.svg b/app/public/404/stick3.svg new file mode 100644 index 0000000..d7c0bb7 --- /dev/null +++ b/app/public/404/stick3.svg @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/public/502.html b/app/public/502.html new file mode 100644 index 0000000..5f25659 --- /dev/null +++ b/app/public/502.html @@ -0,0 +1,74 @@ + + + + + + 502 Bad Gateway + + + +
+ +
502
+
Gateway error.
The website server has timed out in returning data, please wait a few minutes and try again.
+ +
+ + diff --git a/app/public/apple-touch-icon.png b/app/public/apple-touch-icon.png new file mode 100644 index 0000000..d81aacb Binary files /dev/null and b/app/public/apple-touch-icon.png differ diff --git a/app/public/brand/brush.svg b/app/public/brand/brush.svg new file mode 100644 index 0000000..e447d82 --- /dev/null +++ b/app/public/brand/brush.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/public/brand/carlos-arias-signature.svg b/app/public/brand/carlos-arias-signature.svg new file mode 100644 index 0000000..69d9eb7 --- /dev/null +++ b/app/public/brand/carlos-arias-signature.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/public/carlos-arias-dark.jpg b/app/public/carlos-arias-dark.jpg new file mode 100644 index 0000000..b1c1fa4 Binary files /dev/null and b/app/public/carlos-arias-dark.jpg differ diff --git a/app/public/carlos-arias-light.jpg b/app/public/carlos-arias-light.jpg new file mode 100644 index 0000000..f1fcbb6 Binary files /dev/null and b/app/public/carlos-arias-light.jpg differ diff --git a/app/public/carlos-arias-resume.pdf b/app/public/carlos-arias-resume.pdf new file mode 100644 index 0000000..1207744 Binary files /dev/null and b/app/public/carlos-arias-resume.pdf differ diff --git a/app/public/favicon-32.png b/app/public/favicon-32.png new file mode 100644 index 0000000..e20b3e9 Binary files /dev/null and b/app/public/favicon-32.png differ diff --git a/app/public/favicon.ico b/app/public/favicon.ico new file mode 100644 index 0000000..6c741cd Binary files /dev/null and b/app/public/favicon.ico differ diff --git a/app/public/favicon.svg b/app/public/favicon.svg new file mode 100644 index 0000000..542cc92 --- /dev/null +++ b/app/public/favicon.svg @@ -0,0 +1,5 @@ + + + + diff --git a/app/public/icon-192.png b/app/public/icon-192.png new file mode 100644 index 0000000..af25229 Binary files /dev/null and b/app/public/icon-192.png differ diff --git a/app/public/icon-512.png b/app/public/icon-512.png new file mode 100644 index 0000000..f8e2aaf Binary files /dev/null and b/app/public/icon-512.png differ diff --git a/app/public/og-image.jpg b/app/public/og-image.jpg new file mode 100644 index 0000000..ad2c0aa Binary files /dev/null and b/app/public/og-image.jpg differ diff --git a/app/public/portfolio/ginny-goldman.jpg b/app/public/portfolio/ginny-goldman.jpg new file mode 100644 index 0000000..e9f6c2d Binary files /dev/null and b/app/public/portfolio/ginny-goldman.jpg differ diff --git a/app/src/components/BrandMark.astro b/app/src/components/BrandMark.astro new file mode 100644 index 0000000..8ad71b2 --- /dev/null +++ b/app/src/components/BrandMark.astro @@ -0,0 +1,73 @@ +--- +/** + * The signature wordmark, optionally writing itself on load. + * + * A signature is a brush stroke and a personal seal at once, which is why + * it carries the sumi-e direction better than any lettering could. The + * write-on is a left-to-right reveal — the direction of writing — and it + * says "autonomous" by drawing itself without a hand on it. + * + * The SVG is a single-colour black wordmark, so it inverts for dark + * grounds rather than shipping a second file. + */ +const { + width = "148px", + animate = false, + invert = false, + seal = true, + class: className = "", +} = Astro.props; +--- + + + Carlos Arias + {seal && } + + + diff --git a/app/src/components/Footer.astro b/app/src/components/Footer.astro index 02bb596..63925f9 100644 --- a/app/src/components/Footer.astro +++ b/app/src/components/Footer.astro @@ -1,6 +1,14 @@ --- import Icon from "./Icon.astro"; -import { SITE, categories } from "../lib/blog-data.js"; +import { SITE, authors } from "../lib/blog-data.js"; + +// Social profiles come from site.config.json -> author.sameAs, which is also +// what feeds the Person JSON-LD. One list, not two. +const socials = (authors[0]?.sameAs ?? []).map((href) => { + const host = new URL(href).hostname.replace(/^www\./, ""); + const label = host.split(".")[0]; + return { href, label: label.charAt(0).toUpperCase() + label.slice(1) }; +}); const { flush = false } = Astro.props; --- @@ -12,19 +20,42 @@ const { flush = false } = Astro.props;

{SITE.description}

-
- - +
+ { + socials.map((s) => ( + + {s.label} + + )) + } + RSS
-
Sections
+
Services
    { - categories.slice(0, 5).map((category) => ( + [ + { label: "Automation", href: "/services#automation" }, + { label: "Website Design", href: "/services#website-design" }, + { label: "Digital Marketing", href: "/services#digital-marketing" }, + { label: "For Law Firms", href: "/services" }, + ].map((s) => (
  • - - {category.name} + + {s.label}
  • )) @@ -35,10 +66,10 @@ const { flush = false } = Astro.props;
    The site
diff --git a/app/src/components/Header.astro b/app/src/components/Header.astro index f4c3dc8..a56c7ff 100644 --- a/app/src/components/Header.astro +++ b/app/src/components/Header.astro @@ -1,13 +1,15 @@ --- import Icon from "./Icon.astro"; +import { ThemeToggle } from "@/components/ui/be-ui-theme-toggle"; +import { CurvedMenu } from "@/components/ui/curved-menu"; import { SITE } from "../lib/blog-data.js"; const nav = [ - { to: "/", label: "Home" }, - { to: "/blog", label: "Writing" }, - { to: "/about", label: "About" }, { to: "/services", label: "Services" }, - { to: "/contact", label: "Contact" }, + { to: "/projects", label: "My Projects" }, + { to: "/about", label: "About" }, + { to: "/resume", label: "Resume" }, + { to: "/contact", label: "Contact Me" }, ]; const current = Astro.url.pathname.replace(/\/$/, "") || "/"; @@ -26,8 +28,15 @@ const iconClass = isHome
- - {SITE.name} + + +
@@ -101,54 +107,20 @@ const iconClass = isHome -
diff --git a/app/src/components/IndexRow.astro b/app/src/components/IndexRow.astro new file mode 100644 index 0000000..ed9ab26 --- /dev/null +++ b/app/src/components/IndexRow.astro @@ -0,0 +1,47 @@ +--- +/** + * One row of a typographic index. + * + * Used by the work list, the projects page, and anywhere else a list of + * linked things needs the house treatment. Cards are the default on most + * sites and they are wrong here — this is a quiet, scannable index. + * + * Styling lives in styles.css under @layer components (.ca-entry / .ca-tick) + * so a change to hover behaviour is one edit, not one per page. + */ +const { href, title, meta, summary, tags = [], first = false } = Astro.props; +--- + + + + + {title} + + { + meta && ( + + {meta} + + ) + } + + { + summary && ( + + {summary} + + ) + } + + { + tags.length > 0 && ( + + {tags.map((tag) => ( + + {tag} + + ))} + + ) + } + diff --git a/app/src/components/InkWash.astro b/app/src/components/InkWash.astro new file mode 100644 index 0000000..5688961 --- /dev/null +++ b/app/src/components/InkWash.astro @@ -0,0 +1,142 @@ +--- +/** + * Ink diffusing into wet paper. + * + * Follows the pointer, and keeps drifting on its own when nobody is + * touching it — the system runs whether or not you are watching. That is + * the "agentic" idea expressed in ink rather than neon: no particles, no + * constellation lines, no glow. + * + * Reads --ca-ink from its own parent rather than :root, because it sits + * inside .ca-ink-surface where that token is the light value. Reading + * :root would paint near-black onto near-black and show nothing. + */ +--- + + + + + + diff --git a/app/src/components/SectionHead.astro b/app/src/components/SectionHead.astro new file mode 100644 index 0000000..833c175 --- /dev/null +++ b/app/src/components/SectionHead.astro @@ -0,0 +1,23 @@ +--- +/** + * Section heading with optional right-aligned meta. + * + * `brush` puts the vermillion sumi stroke under the title. Use it on + * emphasis only — the brand guide is explicit that brushing every heading + * is the same as brushing none. + */ +const { title, meta, brush = false, id } = Astro.props; +--- + +
+

+ {brush ? {title} : title} +

+ { + meta && ( + + {meta} + + ) + } +
diff --git a/app/src/components/ui/badge.tsx b/app/src/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/app/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/app/src/components/ui/be-ui-theme-toggle.tsx b/app/src/components/ui/be-ui-theme-toggle.tsx new file mode 100644 index 0000000..bf86256 --- /dev/null +++ b/app/src/components/ui/be-ui-theme-toggle.tsx @@ -0,0 +1,248 @@ +"use client"; + +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; +import { motion, useReducedMotion } from "motion/react"; +import { + useEffect, + useState, + type ComponentPropsWithoutRef, + type ReactNode, +} from "react"; +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export const EASE_OUT = [0.16, 1, 0.3, 1] as const; +export const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const; +export const EASE_DRAWER = [0.32, 0.72, 0, 1] as const; + +export const EASE_OUT_CSS = "cubic-bezier(0.16, 1, 0.3, 1)"; + +export const SPRING_PRESS = { + type: "spring", + stiffness: 500, + damping: 30, + mass: 0.6, +} as const; + +export const SPRING_SWAP = { + type: "spring", + stiffness: 460, + damping: 30, + mass: 0.55, +} as const; + +export const SPRING_PANEL = { + type: "spring", + stiffness: 420, + damping: 40, + mass: 0.5, +} as const; + +export const SPRING_LAYOUT = { + type: "spring", + stiffness: 360, + damping: 32, + mass: 0.6, +} as const; + +export const SPRING_MOUSE = { + stiffness: 200, + damping: 15, + mass: 0.3, +} as const; + +export type ThemeVariant = "rectangle" | "circle" | "circle-blur"; + +export type RectStart = + | "top-left" + | "top-right" + | "bottom-left" + | "bottom-right" + | "center" + | "bottom-up"; + +export interface ThemeToggleProps + extends Omit, "children" | "onClick"> { + variant?: ThemeVariant; + start?: RectStart; + iconClassName?: string; +} + +interface ActionSwapIconProps { + value: string; + animation?: "blur"; + className?: string; + children: ReactNode; +} + +function ActionSwapIcon({ value, className, children }: ActionSwapIconProps) { + const reduce = useReducedMotion(); + + return ( + + {children} + + ); +} + +const VT_STYLE_ID = "beui-theme-toggle-vt"; + +const VT_CSS = ` +html[data-beui-vt="rect"]::view-transition-old(root) { + animation: none; + mix-blend-mode: normal; +} +html[data-beui-vt="rect"]::view-transition-new(root) { + mix-blend-mode: normal; + animation: beui-rect-reveal 400ms ease-out; +} +html[data-beui-vt="circle"]::view-transition-old(root), +html[data-beui-vt="circle-blur"]::view-transition-old(root) { + animation: none; + mix-blend-mode: normal; +} +html[data-beui-vt="circle"]::view-transition-new(root) { + mix-blend-mode: normal; + animation: beui-circle-reveal 700ms cubic-bezier(0.4, 0, 0.2, 1); +} +html[data-beui-vt="circle-blur"]::view-transition-new(root) { + mix-blend-mode: normal; + animation: beui-circle-blur-reveal 700ms cubic-bezier(0.4, 0, 0.2, 1); +} +@keyframes beui-rect-reveal { + from { clip-path: var(--beui-vt-from, inset(100% 0 0 0)); } + to { clip-path: inset(0 0 0 0); } +} +@keyframes beui-circle-reveal { + from { clip-path: circle(0% at var(--beui-vt-origin, 50% 100%)); } + to { clip-path: circle(150% at var(--beui-vt-origin, 50% 100%)); } +} +@keyframes beui-circle-blur-reveal { + from { clip-path: circle(0% at var(--beui-vt-origin, 50% 100%)); filter: blur(8px); } + to { clip-path: circle(150% at var(--beui-vt-origin, 50% 100%)); filter: blur(0px); } +} +`; + +const RECT_FROM: Record = { + "top-left": "inset(0 100% 100% 0)", + "top-right": "inset(0 0 100% 100%)", + "bottom-left": "inset(100% 100% 0 0)", + "bottom-right": "inset(100% 0 0 100%)", + center: "inset(50% 50% 50% 50%)", + "bottom-up": "inset(100% 0 0 0)", +}; + +const CIRCLE_ORIGIN: Record = { + "top-left": "0% 0%", + "top-right": "100% 0%", + "bottom-left": "0% 100%", + "bottom-right": "100% 100%", + center: "50% 50%", + "bottom-up": "50% 100%", +}; + +export function useThemeToggle({ + variant = "rectangle", + start = "bottom-up", +}: { + variant?: ThemeVariant; + start?: RectStart; +} = {}) { + const { setTheme, resolvedTheme } = useTheme(); + const reduce = useReducedMotion() ?? false; + const [mounted, setMounted] = useState(false); + + useEffect(() => setMounted(true), []); + + useEffect(() => { + if (document.getElementById(VT_STYLE_ID)) return; + + const el = document.createElement("style"); + el.id = VT_STYLE_ID; + el.textContent = VT_CSS; + document.head.appendChild(el); + }, []); + + const isDark = mounted && resolvedTheme === "dark"; + + const toggle = () => { + const next = isDark ? "light" : "dark"; + + if (reduce || !("startViewTransition" in document)) { + setTheme(next); + return; + } + + const root = document.documentElement; + + if (variant === "rectangle") { + root.style.setProperty("--beui-vt-from", RECT_FROM[start]); + root.dataset.beuiVt = "rect"; + } else { + root.style.setProperty("--beui-vt-origin", CIRCLE_ORIGIN[start]); + root.dataset.beuiVt = variant; + } + + const vt = ( + document as Document & { + startViewTransition(cb: () => void): { finished: Promise }; + } + ).startViewTransition(() => setTheme(next)); + + vt.finished.finally(() => { + delete root.dataset.beuiVt; + }); + }; + + return { isDark, mounted, toggle }; +} + +export function ThemeToggle({ + variant = "rectangle", + start = "bottom-up", + className, + iconClassName, + ...rest +}: ThemeToggleProps) { + const { isDark, mounted, toggle } = useThemeToggle({ variant, start }); + + return ( + + ); +} diff --git a/app/src/components/ui/button.tsx b/app/src/components/ui/button.tsx new file mode 100644 index 0000000..36496a2 --- /dev/null +++ b/app/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/app/src/components/ui/curved-menu.tsx b/app/src/components/ui/curved-menu.tsx new file mode 100644 index 0000000..336181c --- /dev/null +++ b/app/src/components/ui/curved-menu.tsx @@ -0,0 +1,261 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { cn } from "@/lib/utils"; + +/** + * Full-page mobile menu with a curved reveal. + * + * The panel rises behind an SVG whose leading edge starts bowed and flattens + * as it lands — a curtain with weight, not a rectangle sliding up. + * + * Two things that are easy to get wrong here: + * + * · The dialog must NOT paint its own background. .ca-ink-surface normally + * sets background-color, which would make the panel opaque before the + * curtain ever moves and hide the entire effect. The class is kept for its + * token overrides only; the SVG paints the ground. + * + * · The panel MUST be portalled to . It lives inside
, which + * carries backdrop-blur on inner pages — and an ancestor with backdrop-filter + * (or transform, or filter) becomes the containing block for position:fixed + * descendants. Without the portal the dialog resolves `inset-0` against the + * 64px-tall header and renders as a thin strip. The homepage header has no + * blur, which is why the bug only ever showed on inner pages. + * + * · Everything needs an exit, not just the curtain. Without one the copy + * hangs in mid-air over the page beneath while the curtain slides away, + * which reads as lag rather than animation. Content leaves first and + * faster, then the curtain follows it down. + */ + +export interface CurvedMenuItem { + to: string; + label: string; +} + +export interface CurvedMenuProps { + items: CurvedMenuItem[]; + current?: string; + triggerClassName?: string; + email?: string; +} + +/** Bowed leading edge → flat. Same command structure so the path can tween. */ +const CURVE_BOWED = "M0 34 Q 50 -6 100 34 L100 100 L0 100 Z"; +const CURVE_FLAT = "M0 0 Q 50 0 100 0 L100 100 L0 100 Z"; + +const EASE = [0.33, 0.1, 0.25, 1] as const; +const OPEN_MS = 0.62; +const CLOSE_MS = 0.42; // leaving is quicker than arriving — it always should be + +export function CurvedMenu({ + items, + current = "/", + triggerClassName, + email = "hi@carlosarias.com", +}: CurvedMenuProps) { + const [open, setOpen] = useState(false); + const reduce = useReducedMotion() ?? false; + const panelRef = useRef(null); + const triggerRef = useRef(null); + + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + + useEffect(() => { + if (!open) return; + const previous = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = previous; + }; + }, [open]); + + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") close(); + }; + document.addEventListener("keydown", onKey); + return () => document.removeEventListener("keydown", onKey); + }, [open]); + + useEffect(() => { + if (open) panelRef.current?.focus(); + }, [open]); + + const close = () => { + setOpen(false); + triggerRef.current?.focus(); + }; + + const isActive = (to: string) => + to === "/" ? current === "/" : current.startsWith(to); + + const bar = "absolute left-0 h-[1.5px] w-full rounded-full bg-current"; + + return ( + <> + {/* The trigger is also the close control: it lifts above the overlay + when open and its two bars cross into an X. */} + + + {mounted && + createPortal( + + {open && ( + + {reduce ? ( +
+ ) : ( + + )} + + {/* Content leaves first and faster, so nothing is left floating + over the page while the curtain drops. */} + +
+ + + {/* Lives INSIDE the portal, so it never depends on the header's + z-index, ground, or which variant the page rendered. */} + +
+ + + + + + Get in touch + + + {email} + + +
+ + )} + , + document.body, + )} + + ); +} + +export default CurvedMenu; diff --git a/app/src/components/ui/page-not-found.tsx b/app/src/components/ui/page-not-found.tsx new file mode 100644 index 0000000..5c2bbec --- /dev/null +++ b/app/src/components/ui/page-not-found.tsx @@ -0,0 +1,258 @@ +import { useEffect, useRef, useState } from "react"; + +/** + * Animated 404. + * + * Two deliberate changes from the upstream component: + * + * 1. Assets are vendored to /404/*.svg instead of hotlinked from a + * third-party GitHub raw URL. A 404 page that depends on someone + * else's repository staying online is a 404 waiting to 404. + * + * 2. react-router's useNavigate is replaced with history.back() and a + * plain anchor. This is a static Astro site; there is no router. + * + * Note on colour: the copy is intentionally black on a black ground, and + * only becomes readable once the white circle animation has washed over + * the screen. That is why the text fades in on a 1200ms delay. + */ +export default function NotFoundPage() { + return ( +
+ + + +
+ ); +} + +function MessageDisplay() { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => setIsVisible(true), 1200); + return () => clearTimeout(timer); + }, []); + + return ( +
+
+
Page Not Found
+
404
+
+ The page you are looking for might have been removed, had its name changed, or is + temporarily unavailable. +
+
+ + + + Go Home + +
+
+
+ ); +} + +type StickFigure = { + top?: string; + bottom?: string; + src: string; + transform?: string; + speedX: number; + speedRotation?: number; +}; + +function CharactersAnimation() { + const charactersRef = useRef(null); + + useEffect(() => { + const stickFigures: StickFigure[] = [ + { top: "0%", src: "/404/stick0.svg", transform: "rotateZ(-90deg)", speedX: 1500 }, + { top: "10%", src: "/404/stick1.svg", speedX: 3000, speedRotation: 2000 }, + { top: "20%", src: "/404/stick2.svg", speedX: 5000, speedRotation: 1000 }, + { top: "25%", src: "/404/stick0.svg", speedX: 2500, speedRotation: 1500 }, + { top: "35%", src: "/404/stick0.svg", speedX: 2000, speedRotation: 300 }, + { bottom: "5%", src: "/404/stick3.svg", speedX: 0 }, + ]; + + const host = charactersRef.current; + if (host) host.innerHTML = ""; + + stickFigures.forEach((figure, index) => { + const stick = document.createElement("img"); + stick.alt = ""; + stick.classList.add("characters"); + stick.style.position = "absolute"; + stick.style.width = "18%"; + stick.style.height = "18%"; + + if (figure.top) stick.style.top = figure.top; + if (figure.bottom) stick.style.bottom = figure.bottom; + stick.src = figure.src; + if (figure.transform) stick.style.transform = figure.transform; + + host?.appendChild(stick); + + if (index === 5) return; + + stick.animate([{ left: "100%" }, { left: "-20%" }], { + duration: figure.speedX, + easing: "linear", + fill: "forwards", + }); + + if (index === 0) return; + + if (figure.speedRotation) { + stick.animate([{ transform: "rotate(0deg)" }, { transform: "rotate(-360deg)" }], { + duration: figure.speedRotation, + iterations: Infinity, + easing: "linear", + }); + } + }); + + return () => { + if (host) host.innerHTML = ""; + }; + }, []); + + return
; +} + +interface Circulo { + x: number; + y: number; + size: number; +} + +function CircleAnimation() { + const canvasRef = useRef(null); + const requestIdRef = useRef(undefined); + const timerRef = useRef(0); + const circulosRef = useRef([]); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const initArr = () => { + circulosRef.current = []; + for (let index = 0; index < 300; index++) { + const randomX = + Math.floor(Math.random() * (canvas.width * 3 - canvas.width * 1.2 + 1)) + + canvas.width * 1.2; + const randomY = + Math.floor(Math.random() * (canvas.height - (canvas.height * -0.2 + 1))) + + canvas.height * -0.2; + circulosRef.current.push({ x: randomX, y: randomY, size: canvas.width / 1000 }); + } + }; + + const draw = () => { + const context = canvas.getContext("2d"); + if (!context) return; + + timerRef.current++; + context.setTransform(1, 0, 0, 1, 0, 0); + + const distanceX = canvas.width / 80; + const growthRate = canvas.width / 1000; + + context.fillStyle = "white"; + context.clearRect(0, 0, canvas.width, canvas.height); + + circulosRef.current.forEach((circulo) => { + context.beginPath(); + + if (timerRef.current < 65) { + circulo.x -= distanceX; + circulo.size += growthRate; + } + + if (timerRef.current > 65 && timerRef.current < 500) { + circulo.x -= distanceX * 0.02; + circulo.size += growthRate * 0.2; + } + + context.arc(circulo.x, circulo.y, circulo.size, 0, Math.PI * 2); + context.fill(); + }); + + if (timerRef.current > 500) { + if (requestIdRef.current) cancelAnimationFrame(requestIdRef.current); + return; + } + + requestIdRef.current = requestAnimationFrame(draw); + }; + + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + timerRef.current = 0; + initArr(); + draw(); + + const handleResize = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + timerRef.current = 0; + if (requestIdRef.current) cancelAnimationFrame(requestIdRef.current); + initArr(); + draw(); + }; + + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + if (requestIdRef.current) cancelAnimationFrame(requestIdRef.current); + }; + }, []); + + return ; +} diff --git a/app/src/components/ui/resume-intro.tsx b/app/src/components/ui/resume-intro.tsx new file mode 100644 index 0000000..7603718 --- /dev/null +++ b/app/src/components/ui/resume-intro.tsx @@ -0,0 +1,257 @@ +"use client"; + +import { motion, type Variants } from "framer-motion"; +import { ArrowUpRight, Download, Mail, User } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; + +/** + * Resume intro block. + * + * Adapted from the 21st.dev glassmorphism portfolio block. The layout is kept — + * two columns, highlight stack, profile card, social list — but every colour, + * radius and shadow is driven by the site's tokens rather than the component's + * own values. + * + * Notably NOT carried over: rounded-3xl, blurred glows, and the layered drop + * shadows. The brand guide rules those out explicitly (near-square 2px radius, + * hairlines instead of elevation), and glassmorphism on a washi ground reads as + * a different site. What survives is the structure and the motion. + */ + +/** + * No brand glyphs. lucide removed them in v1 (they live in simple-icons now), + * and adding a second icon package to draw four logos would be the wrong trade + * on a page built around restraint. Label plus handle already identifies the + * network; a single consistent arrow does the affordance. + */ +type IconName = string; + +export interface ResumeIntroProps { + name: string; + headline: string; + summary: string; + highlights: { title: string; description: string }[]; + social: { label: string; handle: string; href: string; icon: IconName }[]; + email: string; + resumeHref: string; + /** + * Portrait for the light theme. Leave undefined to render the placeholder + * frame; the frame reserves the same space either way, so adding a photo + * later cannot reflow the layout. + */ + photoSrc?: string; + /** + * Optional dark-theme portrait. Both are rendered and CSS shows one, because + * the theme is class-based (.dark on ) rather than media-query based — + * a would ignore the site's + * own toggle and follow the OS instead. + */ + photoSrcDark?: string; +} + +const listVariants: Variants = { + hidden: { opacity: 0, y: 16 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.4, staggerChildren: 0.07 }, + }, +}; + +const itemVariants: Variants = { + hidden: { opacity: 0, y: 12 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.35 } }, +}; + +const EASE = [0.33, 0.1, 0.25, 1] as const; + +export function ResumeIntro({ + name, + headline, + summary, + highlights, + social, + email, + resumeHref, + photoSrc, + photoSrcDark, +}: ResumeIntroProps) { + return ( +
+
+ + {/* kintsugi seam, in place of the original's gradient wash */} + +
+
+ ); +} + +export default ResumeIntro; diff --git a/app/src/config/capabilities.json b/app/src/config/capabilities.json new file mode 100644 index 0000000..99a93ee --- /dev/null +++ b/app/src/config/capabilities.json @@ -0,0 +1,30 @@ +[ + { + "title": "Agentic AI systems", + "summary": "Multi-agent architectures where AI agents collaborate to solve complex business problems end to end." + }, + { + "title": "AI automation", + "summary": "Replacing repetitive business processes with intelligent, autonomous workflows." + }, + { + "title": "Autonomous websites", + "summary": "Sites that continuously improve their own SEO, content, UX, and conversions using AI." + }, + { + "title": "AI product engineering", + "summary": "AI-first SaaS platforms designed and shipped from concept to production." + }, + { + "title": "Intelligent marketing systems", + "summary": "AI that runs technical SEO, content planning, competitor research, and optimization on its own." + }, + { + "title": "Software architecture", + "summary": "APIs, cloud architecture, automation pipelines, distributed systems, and scalable backends." + }, + { + "title": "Fractional CTO", + "summary": "Technology leadership for startups building AI products." + } +] diff --git a/app/src/config/changelog.json b/app/src/config/changelog.json new file mode 100644 index 0000000..0c3bc2a --- /dev/null +++ b/app/src/config/changelog.json @@ -0,0 +1,182 @@ +[ + { + "type": "updated", + "summary": "Navigation", + "detail": "Added Services to the main menu, front and centre.", + "entry_at": "2026-07-23T16:12" + }, + { + "type": "updated", + "summary": "Contact form aligned to services", + "detail": "Enquiry types now mirror the three core services offered.", + "entry_at": "2026-07-23T16:10" + }, + { + "type": "added", + "summary": "Services page", + "detail": "Automation, website design, and digital marketing — built for law firms and service businesses.", + "entry_at": "2026-07-23T16:05" + }, + { + "type": "added", + "summary": "Footer service links", + "detail": "Each service links straight to the contact form with the enquiry already selected.", + "entry_at": "2026-07-23T15:35" + }, + { + "type": "updated", + "summary": "About headline tuned for search", + "detail": "Led with agentic AI and automation instead of the origin year — same story, better discoverability.", + "entry_at": "2026-07-23T15:20" + }, + { + "type": "added", + "summary": "This changelog", + "detail": "The site now documents its own evolution — fittingly, an agentic website that logs itself.", + "entry_at": "2026-07-23T15:05" + }, + { + "type": "added", + "summary": "Project galleries and social rail", + "detail": "A photo gallery with a lightbox, and a sticky Instagram rail beside each case study.", + "entry_at": "2026-07-23T14:50" + }, + { + "type": "fixed", + "summary": "About copy corrected", + "detail": "Separated learning to code (1988) from building software professionally (1999).", + "entry_at": "2026-07-23T14:33" + }, + { + "type": "added", + "summary": "About page", + "detail": "The story from a Commodore 64 in 1988 to agentic AI today.", + "entry_at": "2026-07-23T13:52" + }, + { + "type": "fixed", + "summary": "Menu portalled to the page root", + "detail": "The overlay now escapes the header entirely and covers the page identically everywhere.", + "entry_at": "2026-07-23T12:45" + }, + { + "type": "added", + "summary": "Project filtering", + "detail": "Filter by category with an animated, shareable, deep-linkable transition.", + "entry_at": "2026-07-23T12:44" + }, + { + "type": "added", + "summary": "Projects moved to the database", + "detail": "Per-project case-study pages generated from cja_projects at build time.", + "entry_at": "2026-07-23T12:30" + }, + { + "type": "added", + "summary": "Contact form wired up", + "detail": "Real submissions stored in the database, with validation and spam protection.", + "entry_at": "2026-07-23T12:14" + }, + { + "type": "added", + "summary": "Portraits with light/dark variants", + "detail": "A wardrobe swap — the right shot for each theme, graded into the palette.", + "entry_at": "2026-07-22T15:51" + }, + { + "type": "added", + "summary": "Resume page", + "detail": "Experience, ventures, a grouped skills breakdown, education, and a downloadable PDF.", + "entry_at": "2026-07-22T15:24" + }, + { + "type": "bug", + "summary": "A component import broke the build", + "detail": "Aliasing framer-motion to its successor created a circular module reference.", + "entry_at": "2026-07-22T15:24" + }, + { + "type": "fixed", + "summary": "Motion dependency resolved", + "detail": "Declared the real package directly instead of aliasing, ending the loop.", + "entry_at": "2026-07-22T15:24" + }, + { + "type": "bug", + "summary": "Mobile menu clipped to the header", + "detail": "On inner pages the header's backdrop-blur trapped the fixed overlay to a thin strip.", + "entry_at": "2026-07-22T13:44" + }, + { + "type": "added", + "summary": "Full-page mobile menu", + "detail": "A curved reveal with the hamburger morphing into a close control.", + "entry_at": "2026-07-22T13:25" + }, + { + "type": "added", + "summary": "Animated 404 page", + "detail": "A playful full-screen 404, with a reduced-motion fallback and locally-hosted assets.", + "entry_at": "2026-07-22T13:12" + }, + { + "type": "bug", + "summary": "Hero rendered paper-on-paper in light mode", + "detail": "A scoped colour override could not reach the already-resolved theme tokens.", + "entry_at": "2026-07-22T12:58" + }, + { + "type": "fixed", + "summary": "Ink surface scoping corrected", + "detail": "The dark hero now overrides the compiled colour tokens directly, in both themes.", + "entry_at": "2026-07-22T12:58" + }, + { + "type": "added", + "summary": "Theme toggle", + "detail": "Light and dark, with an ink-reveal transition that spreads from the button you click.", + "entry_at": "2026-07-22T12:49" + }, + { + "type": "fixed", + "summary": "Schema install repaired", + "detail": "Added the standard foreign-key guard to the snapshot migration so tables load in any order.", + "entry_at": "2026-07-22T12:42" + }, + { + "type": "bug", + "summary": "Base schema failed to install", + "detail": "A migration declared a foreign key before the table it referenced existed.", + "entry_at": "2026-07-22T12:40" + }, + { + "type": "added", + "summary": "Navigation and brand mark", + "detail": "Header signature paired with the hanko seal; nav updated to Projects, Resume, About, Blog, Contact.", + "entry_at": "2026-07-22T12:16" + }, + { + "type": "added", + "summary": "SeedProject cloned and wired", + "detail": "Astro frontend, PHP API, and MariaDB connected end to end.", + "entry_at": "2026-07-22T12:05" + }, + { + "type": "added", + "summary": "React added as Astro islands", + "detail": "Interactive components hydrate only where used; the rest of the site stays static.", + "entry_at": "2026-07-22T12:00" + }, + { + "type": "added", + "summary": "Homepage built", + "detail": "Ink hero with the signature writing itself, a typographic work index, and a full-bleed mission line.", + "entry_at": "2026-07-22T09:15" + }, + { + "type": "added", + "summary": "Brand system established", + "detail": "Sumi-e direction — ink on washi paper, a single vermillion seal. Colour, type, and motion tokens.", + "entry_at": "2026-07-21T16:40" + } +] diff --git a/app/src/config/projects.json b/app/src/config/projects.json new file mode 100644 index 0000000..b8b646e --- /dev/null +++ b/app/src/config/projects.json @@ -0,0 +1,82 @@ +[ + { + "slug": "agentic-marketing-platform", + "title": "Agentic Marketing Platform", + "period": "In development", + "status": "building", + "summary": "An autonomous digital marketing platform where collaborative agents run technical SEO, competitor research, content planning, local SEO, analytics, and reporting — the full process, run by agents.", + "tags": [ + "Multi-agent", + "SEO", + "Analytics" + ], + "categories": [ + "Agentic", + "SaaS", + "Tools" + ] + }, + { + "slug": "medellin-co", + "title": "Medellin.co", + "period": "2020 — present", + "status": "live", + "summary": "An AI-powered city platform using RAG, intelligent search, autonomous content generation, and AI assistants for travelers, expats, and local businesses.", + "tags": [ + "RAG", + "Search", + "Content" + ], + "categories": [ + "Websites", + "SaaS", + "Agentic" + ] + }, + { + "slug": "snoopi-io", + "title": "Snoopi.io", + "period": "2014 — present", + "status": "live", + "summary": "A geolocation SaaS platform delivering IP intelligence, APIs, and location services for developers and enterprise applications.", + "tags": [ + "SaaS", + "APIs", + "Geospatial" + ], + "categories": [ + "SaaS", + "Tools" + ] + }, + { + "slug": "crawllr", + "title": "Crawllr", + "period": "2025 — present", + "status": "live", + "summary": "An intelligent web crawler built for large-scale data collection, knowledge extraction, and AI indexing.", + "tags": [ + "Crawling", + "Knowledge extraction" + ], + "categories": [ + "Tools", + "Agentic" + ] + }, + { + "slug": "gabii", + "title": "Gabii", + "period": "In development", + "status": "building", + "summary": "An AI-powered relationship platform using intelligent profiling, compatibility analysis, and autonomous matchmaking.", + "tags": [ + "Profiling", + "Matching" + ], + "categories": [ + "SaaS", + "Agentic" + ] + } +] diff --git a/app/src/config/resume.json b/app/src/config/resume.json new file mode 100644 index 0000000..158a302 --- /dev/null +++ b/app/src/config/resume.json @@ -0,0 +1,368 @@ +{ + "headline": "Software Engineer · Product Engineer · Digital Marketing Strategist", + "summary": "Over 25 years in software engineering and digital marketing, building scalable SaaS platforms and leading cross-functional teams. In recent years I've focused on AI and machine learning — generative AI, LLMs, vector databases, embedding models, and the full RAG pipeline — applied in production SaaS across fraud prevention, identity verification, and content generation.", + "contact": { + "phone": "561-460-0660", + "email": "hi@carlosarias.com", + "site": "carlosarias.co" + }, + "social": [ + { + "label": "LinkedIn", + "handle": "@hicarlosarias", + "href": "https://www.linkedin.com/in/hicarlosarias/", + "icon": "linkedin" + }, + { + "label": "X", + "handle": "@hicarlosarias", + "href": "https://x.com/hicarlosarias", + "icon": "x" + }, + { + "label": "Instagram", + "handle": "@carlosarias.co", + "href": "https://instagram.com/carlosarias.co", + "icon": "instagram" + }, + { + "label": "YouTube", + "handle": "@hiCarlosArias", + "href": "https://www.youtube.com/@hiCarlosArias", + "icon": "youtube" + }, + { + "label": "GitHub", + "handle": "@CarlosJa", + "href": "https://github.com/CarlosJa", + "icon": "github" + } + ], + "highlights": [ + { + "title": "Focus", + "description": "Agentic AI and automation — multi-agent systems that research, plan, build, and optimise with minimal human intervention." + }, + { + "title": "Industries", + "description": "FinTech since 2014 · healthcare · SaaS · legal · geospatial. Security and compliance as a default: HIPAA, SOC 2, PCI." + }, + { + "title": "Track record", + "description": "Grew a credit-reporting SaaS past $1M ARR · manages $1M+ in annual ad spend · 200+ pages ranked for Medellin.co." + } + ], + "experience": [ + { + "role": "Digital Marketing Strategist & Engineer", + "org": "CarlosArias & Co", + "sector": "Independent", + "period": "Jul 1999 — Present", + "current": true, + "summary": "Fractional marketing director for law firms across personal injury, family law, bankruptcy, and tax resolution. Full-funnel strategy — SEO, PPC, Google Business Profile, local service pages, LSA, conversion tracking, and OTT. Current focus is generative SEO, AI integration, and scalable marketing frameworks that improve organic case acquisition.", + "points": [ + "SEO strategy and content built to rank in AI engines, not just search engines", + "Manages over $1M in PPC campaigns with partners including DrivenLeads and WordStream", + "Link building, digital PR, and directory strategy executed through overseas teams", + "Analytics and Search Console instrumentation for end-to-end attribution", + "Tooling: CallRail, Swydo, n8n, SEMrush, Ahrefs, ClickUp, Notion" + ] + }, + { + "role": "Digital Marketing Director & SEO Specialist", + "org": "CreditGuard of America", + "sector": "Debt consolidation", + "period": "Jan 2022 — Jan 2025", + "summary": "Inherited the marketing function for CreditGuard, sister company to GNUSA. Built the strategy to generate leads, rebuild the site and landing pages, and drive calls into the call centre.", + "points": [ + "Increased organic traffic 35% and improved lead generation", + "Lowered CPA by 70% and increased lead conversion by 30%", + "Email programme reaching up to 70% open rate", + "Built the affiliate marketing programme from scratch", + "Ran PPC across Google, Bing, and Meta in synergy with SEO" + ] + }, + { + "role": "Lead Software Engineer II", + "org": "GNUSA", + "sector": "FinTech", + "period": "Jun 2016 — Jan 2022", + "summary": "Brought on to build a SaaS platform and maintain legacy systems. Took the credit-reporting SaaS concept from MVP through product launch and growth.", + "points": [ + "Architected and engineered the GNUSA credit-reporting SaaS platform", + "Grew the platform past $1M ARR", + "Integrated Equifax and TransUnion credit bureaus", + "Database administration: security, ERD design, restoration, and compliance", + "Led overseas development teams and coordinated feature delivery" + ] + }, + { + "role": "Chief Technology Officer (Fractional)", + "org": "GypsyNurse", + "sector": "Nursing staffing", + "period": "Jun 2016 — Jan 2017", + "summary": "Part-time CTO taking over development of a new platform, managing the overseas team and keeping technology aligned to the business vision.", + "points": [ + "Owned delivery of the platform on time and on budget", + "Third-party API and payment gateway integration", + "Reported status and budget to shareholders", + "Led and coordinated the overseas engineering team" + ] + }, + { + "role": "Chief Technology Officer (Fractional)", + "org": "HealthCareForYou", + "sector": "Health insurance", + "period": "Jun 2014 — Jan 2017", + "summary": "Part-time CTO moving an insurance business from a traditional process to digital sales, and aligning it with Healthcare.gov to be first to market with online healthcare signups.", + "points": [ + "Integrated the business with Healthcare.gov / ACA enrolment", + "First to market in their segment with online healthcare signup", + "Database security, ERD design, and compliance", + "Led overseas development teams" + ] + }, + { + "role": "SEO Specialist & Sr. Full-Stack Developer", + "org": "TouchSuite", + "sector": "Payments", + "period": "Jun 2011 — Aug 2014", + "summary": "Hired to build the SEO and web development department from nothing, and to move the company from traditional sales into digital.", + "points": [ + "Built and led the web development and SEO departments", + "Digitised the sales process by integrating Equifax and DocuSign into signup", + "Hired and managed both SEO and engineering teams", + "Raised conversion through high-intent landing pages and SEO" + ] + } + ], + "projects": [ + { + "name": "Medellin.co", + "kind": "SaaS / Media", + "period": "Since Oct 2022", + "summary": "The largest resource hub for Medellín, Colombia — used by locals, expats, and tourists. Acquired the domain from its previous owner after the city released it.", + "points": [ + "200+ pages optimised, roughly 70% ranking on page one", + "Domain authority grown from 10 to 30", + "Chatbot on OpenAI LLM with Zilliz vector DB and OpenAI embeddings", + "Custom PHP integrating directly with TikTok and Instagram", + "Direct integration with law enforcement for filing criminal reports", + "Stripe checkout for advertisers" + ] + }, + { + "name": "Verificaa.com", + "kind": "SaaS", + "period": "Since Jun 2021", + "summary": "ID verification and fraud prevention for Latin America, letting people verify businesses and individuals before doing business with them. The platform has helped save two lives and prevented losses for numerous businesses.", + "points": [ + "AI and LLM-driven fraud detection infrastructure", + "Integrations with police, attorney general, and business databases", + "Data-mining pipeline that crawls and normalises open Colombian and LATAM data", + "REST API with API key management", + "Custom PHP dashboard with complex user management" + ] + }, + { + "name": "Snoopi.io", + "kind": "SaaS", + "period": "Since Mar 2015", + "summary": "GeoIP location and IP fraud prevention. Started as a way to learn PHP OOP and build a REST API; ten years later, rebuilt with a modern dashboard and AI analysis on the roadmap.", + "points": [ + "Real-time tracking script for sites and landing pages", + "GeoIP-driven landing page optimisation with AI", + "Custom dashboard, invoice and subscription management", + "Stripe integration with a custom card form" + ] + }, + { + "name": "Crypto AI Trading Bot", + "kind": "Private", + "period": "Since Mar 2022", + "summary": "A personal bot that follows a defined trading strategy, since upgraded to bring AI into the decision loop.", + "points": [ + "Binance integration, moving to Bitfinex", + "Real-time charting with custom buy/sell algorithms", + "LLM monitoring of market conditions" + ] + } + ], + "skills": [ + { + "group": "Engineering", + "items": [ + { + "name": "Polyglot programming", + "years": 25 + }, + { + "name": ".NET, ASP, PHP, Java, Python, Bash", + "years": 25 + }, + { + "name": "Database design & architecture", + "years": 20 + }, + { + "name": "API integration", + "years": 20 + }, + { + "name": "Cloud architecture (Vultr, AWS)", + "years": 10 + }, + { + "name": "Cybersecurity & data protection", + "years": 10 + }, + { + "name": "Linux administration (L1–L2)", + "years": 10 + }, + { + "name": "React, TypeScript, JavaScript", + "years": 7 + } + ] + }, + { + "group": "AI", + "items": [ + { + "name": "OpenAI, Mistral, Anthropic, Llama", + "years": 3 + }, + { + "name": "LLM integration & prompting", + "years": 2 + }, + { + "name": "RAG, vector DBs, embeddings", + "years": 2 + }, + { + "name": "Agentic systems & orchestration", + "years": 2 + } + ] + }, + { + "group": "Marketing", + "items": [ + { + "name": "Search engine optimisation", + "years": 25 + }, + { + "name": "Graphic design", + "years": 25 + }, + { + "name": "Digital marketing", + "years": 20 + }, + { + "name": "Traditional marketing", + "years": 20 + }, + { + "name": "Digital marketing strategy", + "years": 15 + }, + { + "name": "PPC — Google, Bing, Meta", + "years": 15 + }, + { + "name": "UX & usability", + "years": 10 + }, + { + "name": "Social media strategy", + "years": 5 + } + ] + }, + { + "group": "Leadership", + "items": [ + { + "name": "Technical team leadership" + }, + { + "name": "Hiring & team development" + }, + { + "name": "Project management" + }, + { + "name": "SaaS launch strategy" + }, + { + "name": "Project & department budgeting" + }, + { + "name": "Agile & Scrum" + }, + { + "name": "Technology & business alignment" + }, + { + "name": "Product engineering" + } + ] + } + ], + "tools": [ + "Google Analytics", + "Looker Studio", + "Search Console", + "Ahrefs", + "SEMrush", + "Google Ads", + "Bing Ads", + "Meta Business", + "WooCommerce", + "Magento", + "Zoho CRM", + "Salesforce", + "HubSpot", + "Brevo", + "SendGrid", + "Adobe Suite", + "Figma", + "Sketch", + "Canva", + "CapCut", + "Trello", + "Asana", + "Jira", + "WordPress", + "Stripe", + "Authorize.Net", + "n8n" + ], + "education": [ + { + "title": "BA, Computer Information Systems & Marketing", + "org": "DeVry University", + "period": "Graduated May 2004" + }, + { + "title": "MIT Management — Blockchain", + "org": "Massachusetts Institute of Technology", + "period": "Feb — Mar 2022" + } + ], + "certifications": [ + { + "title": "Google Ads Certification", + "period": "Jan 2024" + }, + { + "title": "Magento 2.0 Certified", + "period": "Mar 2022" + } + ] +} diff --git a/app/src/config/services.json b/app/src/config/services.json new file mode 100644 index 0000000..6568719 --- /dev/null +++ b/app/src/config/services.json @@ -0,0 +1,51 @@ +{ + "services": [ + { + "slug": "automation", + "name": "Automation", + "subject": "automation", + "lead": "AI automation that runs the repetitive work your team does by hand.", + "body": "I build autonomous systems and AI agents that take over the workflows eating your team's time — lead intake, follow-up, research, scheduling, reporting, content. The goal isn't another dashboard; it's software that does the work, so your people spend their time only on what software can't.", + "points": [ + "AI agents that replace whole workflows, not just single tasks", + "Lead intake, qualification, and instant follow-up", + "Research, reporting, and content generated on their own", + "Systems that monitor results and adapt over time" + ] + }, + { + "slug": "website-design", + "name": "Website Design", + "subject": "website-design", + "page": "/services/website-design", + "lead": "Fast, modern websites engineered to convert and rank.", + "body": "Custom sites built for speed, search, and conversion — not dragged out of a template. Engineered to bring in qualified leads, hold up under real traffic, and rank in both search engines and the AI engines that are starting to send traffic. Optionally agentic: a site that improves its own SEO, content, and conversions over time.", + "points": [ + "Custom-built, not templated — fast and search-ready", + "Designed around one thing: turning visitors into leads", + "Local landing pages that rank for the work you want", + "Can be made agentic — a site that optimises itself" + ] + }, + { + "slug": "digital-marketing", + "name": "Digital Marketing", + "subject": "digital-marketing", + "lead": "Technical SEO and marketing systems that bring in cases and customers.", + "body": "Twenty-five years of SEO, PPC, and local search — now run with AI doing the heavy lifting. I build the acquisition engine: technical SEO, Google Business Profile, local service pages, call tracking, and campaigns that turn search traffic into booked work. Managed by intelligent systems, not manual busywork.", + "points": [ + "Technical SEO built to rank in search and AI engines", + "Google Business Profile and local search optimisation", + "PPC and campaign management, tracked end to end", + "Call and conversion tracking, so you know what works" + ] + } + ], + "primaryVertical": "Law firms", + "verticals": [ + { "name": "Law firms", "note": "Personal injury, family, bankruptcy, tax — more qualified cases, faster intake.", "primary": true }, + { "name": "Roofing & home services", "note": "Local search dominance and instant lead follow-up for the trades." }, + { "name": "Accounting", "note": "Steady, qualified enquiries and less time lost to manual admin." }, + { "name": "Service businesses", "note": "Any business that wins or loses on lead volume and response time." } + ] +} diff --git a/app/src/config/site.json b/app/src/config/site.json index 0f6ba33..c27c62e 100644 --- a/app/src/config/site.json +++ b/app/src/config/site.json @@ -1,20 +1,37 @@ { - "name": "SeedProject Site", - "url": "https://example.com", - "description": "A short one-line description of this site, used for SEO and social sharing.", - "tagline": "A starter site built on SeedProject", + "name": "Carlos Arias", + "url": "https://carlosarias.co", + "description": "Agentic AI & Automation Engineer building autonomous software, AI agents, and intelligent business systems.", + "tagline": "Autonomous systems that do the work teams used to do by hand.", "language": "en", "author": { - "slug": "site-author", - "name": "Site Author", - "bio": "One-line author bio — who runs this site.", - "longBio": "A longer author biography paragraph describing who runs this site and their background.", - "avatar": "/avatar-placeholder.png", - "jobTitle": "Founder & Editor", - "knowsAbout": [], - "sameAs": [] + "slug": "carlos-arias", + "name": "Carlos Arias", + "bio": "Agentic AI & Automation Engineer building autonomous software and intelligent business systems.", + "longBio": "Carlos Arias is an Agentic AI & Automation Engineer who builds autonomous software, AI agents, and intelligent business systems. He has been coding since age eight, on a Commodore 64 in 1988, and has spent 26 years as a Software Engineer, CTO, and Technology Advisor across FinTech, healthcare, SaaS, and geospatial technology. Since 2022 his focus has been entirely on agentic AI — designing multi-agent systems that research, plan, build, and optimize on their own.", + "avatar": "/carlos-arias.jpg", + "jobTitle": "Agentic AI & Automation Engineer", + "knowsAbout": [ + "Agentic AI Systems", + "Multi-Agent Collaboration", + "AI Automation", + "Autonomous Websites", + "AI Product Engineering", + "Retrieval-Augmented Generation", + "AI Workflow Orchestration", + "Software Architecture", + "Technical SEO", + "Fractional CTO" + ], + "sameAs": [ + "https://www.linkedin.com/in/hicarlosarias/", + "https://x.com/hicarlosarias", + "https://instagram.com/carlosarias.co", + "https://www.youtube.com/@hiCarlosArias", + "https://github.com/CarlosJa" + ] }, "social": { - "twitter": "" + "twitter": "@hicarlosarias" } } diff --git a/app/src/config/website-design.json b/app/src/config/website-design.json new file mode 100644 index 0000000..fdcb89b --- /dev/null +++ b/app/src/config/website-design.json @@ -0,0 +1,43 @@ +{ + "eyebrow": "Website Design & Development", + "title": "Law firm websites that turn searches into signed cases.", + "lead": "Most legal searches happen on a phone, often from someone who needs help now. Your website has a few seconds to load, earn trust, and make it effortless to call. I build sites that do exactly that.", + "intro": "A law firm website is not a brochure — it is an intake tool. Every decision, from how fast a page loads to where the call button sits, is made to turn a stranger doing a Google search into a booked consultation. I design and build custom sites for firms, engineered for speed, local search, and conversion, with the option to make them agentic so they keep improving on their own.", + "included": [ + { "title": "Built to convert", "note": "Click-to-call, clear intake forms, and calls to action placed where they actually get used — not buried below the fold." }, + { "title": "Fast on mobile", "note": "Most legal searches are mobile and urgent. Pages load in a heartbeat, because a slow site is a lost case." }, + { "title": "Local SEO ready", "note": "Structured to rank for the searches that matter — “personal injury lawyer near me,” practice area plus city — and in AI search too." }, + { "title": "Trust that reads instantly", "note": "Case results, reviews, credentials, and bar admissions surfaced where a nervous prospect looks first." }, + { "title": "Practice-area pages that rank", "note": "A real page for each service you offer, written and structured to win its own searches." }, + { "title": "Optionally agentic", "note": "A site that monitors its own performance and improves its SEO, content, and conversions over time." } + ], + "process": [ + { "step": "01", "title": "Discovery", "note": "Your practice areas, your best cases, the clients you want more of, and what the competition is doing." }, + { "step": "02", "title": "Design", "note": "A look that reads as credible and current — built around intake, not decoration." }, + { "step": "03", "title": "Build & launch", "note": "Fast, custom, search-ready. Migrated carefully so you keep the rankings you already have." }, + { "step": "04", "title": "Optimise", "note": "Track calls and conversions, then keep tuning — manually or with agents doing the work." } + ], + "portfolio": [ + { + "name": "Law Office of Ginny L. Goldman", + "practice": "Business & Contract Law · Boca Raton, FL", + "url": "https://ginnygoldman.com", + "image": "/portfolio/ginny-goldman.jpg", + "note": "A credible, trust-first design for a boutique contract practice, with click-to-call and clear intake front and centre." + }, + { + "name": "Your firm here", + "practice": "Add a project", + "url": "", + "image": "", + "note": "A recent law firm build — screenshot and details to be added." + }, + { + "name": "Your firm here", + "practice": "Add a project", + "url": "", + "image": "", + "note": "A recent law firm build — screenshot and details to be added." + } + ] +} diff --git a/app/src/layouts/BaseLayout.astro b/app/src/layouts/BaseLayout.astro index 6651c1b..08c95e1 100644 --- a/app/src/layouts/BaseLayout.astro +++ b/app/src/layouts/BaseLayout.astro @@ -27,7 +27,9 @@ const absoluteUrl = (value) => { } }; const canonicalUrl = absoluteUrl(canonical || Astro.url.pathname); -const ogImageUrl = absoluteUrl(ogImage); +// Default share card (the branded portrait + tagline). Any page can override +// by passing its own `ogImage`. +const ogImageUrl = absoluteUrl(ogImage || "/og-image.jpg"); --- @@ -44,11 +46,21 @@ const ogImageUrl = absoluteUrl(ogImage); {canonicalUrl && } {ogImageUrl && } + {ogImageUrl && } + {ogImageUrl && } {ogImageUrl && } {canonicalUrl && } + + {/* Favicons — the vermillion 印 seal. SVG for modern browsers, PNG/ICO + fallbacks, and an apple-touch-icon for home-screen bookmarks. */} + + + + + (a[0] < b[0] ? 1 : -1)) + .map(([dateKey, items]) => ({ + dateKey, + // parse as UTC noon to avoid any timezone date-shift + dateLabel: fmt.format(new Date(`${dateKey}T12:00:00Z`)), + items, + })); +} diff --git a/app/src/lib/next-themes.ts b/app/src/lib/next-themes.ts new file mode 100644 index 0000000..2ab1c6e --- /dev/null +++ b/app/src/lib/next-themes.ts @@ -0,0 +1,77 @@ +/** + * A `next-themes` stand-in. + * + * Most shadcn / 21st.dev components import `useTheme` from "next-themes", + * which is a Next.js package and cannot run here. Rather than editing every + * component you paste in — which defeats the point of copy-paste components — + * Vite aliases the "next-themes" specifier to this file (see astro.config.mjs). + * + * It speaks the same API surface the components actually use + * (`theme`, `resolvedTheme`, `setTheme`, `systemTheme`) but drives the + * existing class-based theme already implemented in BaseLayout and Header: + * a `.dark` class on plus a `theme` key in localStorage. + * + * That means the React components and the plain-Astro header stay in sync — + * toggling either one updates the other. + */ +import { useCallback, useEffect, useState } from "react"; + +export type Theme = "light" | "dark" | "system"; + +const STORAGE_KEY = "theme"; + +const prefersDark = () => + typeof window !== "undefined" && + window.matchMedia("(prefers-color-scheme: dark)").matches; + +const currentIsDark = () => + typeof document !== "undefined" && + document.documentElement.classList.contains("dark"); + +export function useTheme() { + // Start undefined so server render and first client render agree; components + // gate on `mounted` before reading it, which is why they all do that dance. + const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark" | undefined>( + undefined, + ); + + useEffect(() => { + setResolvedTheme(currentIsDark() ? "dark" : "light"); + + // Keep in step with the vanilla toggle in Header.astro. + const observer = new MutationObserver(() => { + setResolvedTheme(currentIsDark() ? "dark" : "light"); + }); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }); + + return () => observer.disconnect(); + }, []); + + const setTheme = useCallback((next: Theme) => { + const dark = next === "system" ? prefersDark() : next === "dark"; + document.documentElement.classList.toggle("dark", dark); + try { + if (next === "system") localStorage.removeItem(STORAGE_KEY); + else localStorage.setItem(STORAGE_KEY, next); + } catch { + /* private mode — the class still applies for this session */ + } + setResolvedTheme(dark ? "dark" : "light"); + }, []); + + return { + theme: resolvedTheme as Theme | undefined, + resolvedTheme, + setTheme, + systemTheme: (prefersDark() ? "dark" : "light") as "light" | "dark", + themes: ["light", "dark", "system"] as Theme[], + }; +} + +/** No-op: the theme class is applied by the inline script in BaseLayout. */ +export function ThemeProvider({ children }: { children?: React.ReactNode }) { + return children ?? null; +} diff --git a/app/src/lib/projects.js b/app/src/lib/projects.js new file mode 100644 index 0000000..e4d665e --- /dev/null +++ b/app/src/lib/projects.js @@ -0,0 +1,113 @@ +import mysql from "mysql2/promise"; +import fallback from "../config/projects.json"; + +/** + * Projects, read from cja_projects at BUILD time. + * + * The site is static, so this runs once during `npm run build` and every + * project page ships as real HTML — crawlable, instant, no client-side fetch + * and no loading state. The cost is that content changes need a rebuild, which + * is exactly what the agent pipeline already does. + * + * If the database is unreachable the build does NOT fail: it falls back to + * config/projects.json and logs loudly. A dead database should never be able + * to take the whole site down with it. + */ + +const env = import.meta.env; + +let cache = null; + +const parseJson = (value, whenEmpty) => { + if (!value) return whenEmpty; + if (typeof value === "object") return value; // mysql2 may pre-parse JSON columns + try { + return JSON.parse(value); + } catch { + return whenEmpty; + } +}; + +/** Shape a DB row into what the templates expect. */ +const toProject = (row) => ({ + slug: row.slug, + title: row.title, + kind: row.kind, + period: row.period_label, + status: row.status, + summary: row.summary, + lede: row.lede, + body: row.body, + role: row.role, + categories: parseJson(row.categories, []), + stack: parseJson(row.stack, []), + skills: parseJson(row.skills, []), + metrics: parseJson(row.metrics, []), + gallery: parseJson(row.gallery, []), + links: parseJson(row.links, {}), + cover: row.cover_image || null, + coverAlt: row.cover_alt || "", + seoTitle: row.seo_title || null, + seoDescription: row.seo_description || null, + featured: Boolean(row.featured), +}); + +export async function getProjects() { + if (cache) return cache; + + const host = env.DB_HOST; + const database = env.DB_NAME; + const user = env.DB_USER; + const password = env.DB_PASS; + + if (!host || !database || !user) { + console.warn( + "[projects] No database credentials in env — falling back to config/projects.json", + ); + cache = fallback; + return cache; + } + + let conn; + try { + conn = await mysql.createConnection({ host, user, password, database }); + const [rows] = await conn.execute( + `SELECT * FROM cja_projects + WHERE published = 1 + ORDER BY sort_order ASC, started_on DESC`, + ); + + if (!rows.length) { + console.warn("[projects] cja_projects returned no published rows — using fallback"); + cache = fallback; + } else { + cache = rows.map(toProject); + console.log(`[projects] loaded ${cache.length} from cja_projects`); + } + } catch (error) { + // Loud, but not fatal. A build that silently ships stale content is worse + // than one that tells you the database was unreachable. + console.error(`[projects] DB read failed (${error.message}) — using fallback`); + cache = fallback; + } finally { + await conn?.end(); + } + + return cache; +} + +export async function getProject(slug) { + return (await getProjects()).find((p) => p.slug === slug) ?? null; +} + +/** Category names with counts, derived from the projects themselves. */ +export async function getCategories() { + const projects = await getProjects(); + const counts = {}; + for (const p of projects) { + for (const c of p.categories ?? []) counts[c] = (counts[c] ?? 0) + 1; + } + return Object.entries(counts) + .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) + .map(([name, count]) => ({ name, count, slug: name.toLowerCase() })); +} diff --git a/app/src/lib/utils.ts b/app/src/lib/utils.ts new file mode 100644 index 0000000..50f4b76 --- /dev/null +++ b/app/src/lib/utils.ts @@ -0,0 +1,10 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +/** + * Merge Tailwind class lists, letting later classes win conflicts. + * shadcn and 21st.dev components import this from "@/lib/utils". + */ +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/app/src/pages/.index.astro.orig b/app/src/pages/.index.astro.orig new file mode 100644 index 0000000..0b1b3eb --- /dev/null +++ b/app/src/pages/.index.astro.orig @@ -0,0 +1,225 @@ +--- +import { Image } from "astro:assets"; +import BaseLayout from "../layouts/BaseLayout.astro"; +import PostCard from "../components/PostCard.astro"; +import Newsletter from "../components/Newsletter.astro"; +import Icon from "../components/Icon.astro"; +import heroImage from "../assets/hero-placeholder.png"; +import { + SITE, + categories, + featuredPost, + sortedPosts, + getAuthor, + getCategory, + formatDate, + organizationSchema, + websiteSchema, +} from "../lib/blog-data.js"; + +const featured = await featuredPost(); +const featuredAuthor = getAuthor(featured.author); +const featuredCategory = getCategory(featured.category); +const latest = (await sortedPosts()).filter((post) => post.slug !== featured.slug); +const jsonLd = { + "@context": "https://schema.org", + "@graph": [organizationSchema(), websiteSchema()], +}; +--- + + +
+ Hero image placeholder — replace in app/src/assets +
+ +
+
+
+
Featured
+ All writing → +
+ +
+
+ +
+
+

Latest

+
+ + { + categories.map((category) => ( + + )) + } +
+
+ + + +
+ {latest.map((post, index) =>
+ + +
+ + +
+ + diff --git a/app/src/pages/404.astro b/app/src/pages/404.astro index f625479..f8bbe6b 100644 --- a/app/src/pages/404.astro +++ b/app/src/pages/404.astro @@ -1,21 +1,62 @@ --- -import BaseLayout from "../layouts/BaseLayout.astro"; +// BaseLayout normally pulls this in. This page bypasses BaseLayout (the +// component is a full-screen takeover and would fight the sticky header), +// so the stylesheet has to be imported explicitly — without it none of the +// component's Tailwind classes exist on the page at all. +import "../styles.css"; +import NotFoundPage from "@/components/ui/page-not-found"; +import { SITE } from "../lib/blog-data.js"; + +// Deliberately NOT wrapped in BaseLayout: the component is a full-screen +// takeover and would collide with the sticky header. Everything a visitor +// needs in order to get out is inside the component itself. --- - -
-
404
-

Lost in the margins

-

- The page you're looking for has wandered off. It may have been moved, retitled, or never existed at all. -

- -
-
+ + + + + + Page not found — {SITE.name} + + + + + + + + + + diff --git a/app/src/pages/about.astro b/app/src/pages/about.astro index 2da3839..a46d852 100644 --- a/app/src/pages/about.astro +++ b/app/src/pages/about.astro @@ -1,56 +1,190 @@ --- import BaseLayout from "../layouts/BaseLayout.astro"; -import Breadcrumbs from "../components/Breadcrumbs.astro"; import { SITE, authors } from "../lib/blog-data.js"; + +const author = authors[0]; + +// The instinct that runs through everything — pulled out so the essay can stay +// prose while the eye still catches the shape of the career. +const timeline = [ + { year: "1988", note: "Taught myself to code — Commodore BASIC, age 8" }, + { year: "1990s", note: "Learning the craft: BASIC → Visual Basic → C++ → Java → the web" }, + { year: "1999", note: "Building software professionally" }, + { year: "2000s", note: "Built a CMS from scratch, before WordPress was a thing" }, + { year: "2022", note: "All in on agentic AI" }, +]; + +const jsonLd = { + "@context": "https://schema.org", + "@type": "AboutPage", + mainEntity: { + "@type": "Person", + name: SITE.name, + jobTitle: author.jobTitle, + description: author.bio, + url: SITE.url, + sameAs: author.sameAs, + }, +}; --- -
- + +
+

About

+

+ I automate the work teams used to do by hand, with agentic AI. +

+

+ I’ve done this before it was obvious more than once. This time it feels like + 1988 again. +

+
-
-
About
-

- About {SITE.name}. -

-
- -
-

- This is placeholder copy for your About page. Describe what {SITE.name} is, - who it’s for, and why it exists. Edit this in app/src/pages/about.astro. -

-

What we do

-

- Summarize what visitors will find here — the topics you cover, the value - you offer, and what makes your site worth their time. -

-

How it works

-

- If your site uses the content pipeline or other automation, explain it here. - Otherwise, use this space to describe your process, standards, or approach. -

-
- -
-

The people behind {SITE.name}

-
- { - authors.map((author) => ( - - -
-
{author.name}
-
{author.bio}
-
-
- )) - } + +
+
+ +
+
+ +
+ + Instagram + @carlosarias.co +
+ +
+ + Follow along → +
-
-
+ + +
+

+ I taught myself to code in 1988, on a Commodore 64, at eight years old, using + Commodore BASIC. What began as curiosity quickly became a lifelong obsession + with technology, automation, and figuring out what computers could do next. + I’ve been building software professionally since 1999. +

+

+ As technology evolved, so did I. I moved from BASIC to Visual Basic, C++, Java, + and eventually web application development. By the late 1990s I was building + database-driven applications and eCommerce systems from scratch. By the early + 2000s I had built my own content management system — before platforms like + WordPress made CMS technology mainstream. +

+ +
+ That pattern has followed me my whole career: I see where technology is going + and start building before it becomes obvious. +
+ +

+ For more than 25 years I’ve worked across software engineering, product + development, automation, digital marketing, and technology leadership. + I’ve built SaaS platforms, founded technology companies, led engineering + teams, and developed systems across FinTech, financial services, healthcare, + digital marketing, and other industries. +

+ +

Today, the focus is agentic AI.

+

+ AI has brought me back to the feeling I had when I first discovered programming + in 1988. I’ve worked with automation and intelligent software in different + forms for years, but modern agentic AI fundamentally changed how I think about + and build software. Since 2022 I’ve focused heavily on AI engineering, + autonomous systems, and building intelligent agents that can reason, make + decisions, use tools, coordinate workflows, and perform real work with minimal + human intervention. +

+ +
+ Software that once needed a team and 6–12 months can now, in some cases, + be designed and built by me in 6–12 days. +
+ +

+ My work today goes beyond adding AI features or chatbots to traditional + software. I build autonomous, agentic systems designed to + actively perform work — systems that research, analyse information, make + decisions, interact with APIs and software, generate and modify content, + execute workflows, monitor results, and continuously adapt to what is happening + around them. +

+

+ I’m particularly interested in building AI agents that replace + workflows, not just individual tasks — turning complex business + processes that traditionally need multiple people, tools, and repetitive manual + work into intelligent systems that operate with increasing autonomy. +

+ +

This website is one of them.

+

+ It’s an agentic website. AI agents play an active role in + its development, content, optimisation, maintenance, and evolution. I provide + the vision, architecture, constraints, and strategic direction; AI performs much + of the execution. +

+

+ In many ways, it feels like 1988 again. We’re at the beginning of a new era + of computing. The tools are changing incredibly fast, the rules are still being + written, and once again I find myself experimenting, building, and trying to + understand what technology will be capable of next. +

+
+
+ + + +
+

+ The through-line +

+
    + { + timeline.map((t) => ( +
  1. + {t.year} + {t.note} +
  2. + )) + } +
+
+ + +
+

+ Building the next thing, before it’s obvious. +

+ +
diff --git a/app/src/pages/changelog.astro b/app/src/pages/changelog.astro new file mode 100644 index 0000000..c7e243d --- /dev/null +++ b/app/src/pages/changelog.astro @@ -0,0 +1,82 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import { SITE } from "../lib/blog-data.js"; +import { getChangelogByDay } from "../lib/changelog.js"; + +const days = await getChangelogByDay(); + +const TYPE_LABEL = { + added: "Added", + updated: "Updated", + fixed: "Fixed", + bug: "Bug found", + removed: "Removed", + note: "Note", +}; + +const time = (iso) => iso.slice(11, 16); // HH:MM + +const jsonLd = { + "@context": "https://schema.org", + "@type": "CollectionPage", + name: `Changelog — ${SITE.name}`, + description: "A running log of how this site is built and changed.", + url: `${SITE.url}/changelog`, +}; +--- + + +
+

+ Changelog +

+

+ This site, as it’s built. +

+

+ An agentic website that documents its own evolution. Every change — added, + updated, fixed, and the odd bug caught along the way — logged as it happens. +

+
+ +
+
+ { + days.map((day) => ( +
+

+ + {day.items.length} +

+
    + {day.items.map((e) => ( +
  1. +
  2. + ))} +
+
+ )) + } +
+
+
diff --git a/app/src/pages/contact.astro b/app/src/pages/contact.astro index f80ad08..03f2170 100644 --- a/app/src/pages/contact.astro +++ b/app/src/pages/contact.astro @@ -1,98 +1,205 @@ --- import BaseLayout from "../layouts/BaseLayout.astro"; -import Breadcrumbs from "../components/Breadcrumbs.astro"; -import Icon from "../components/Icon.astro"; -import { SITE } from "../lib/blog-data.js"; +import { SITE, authors } from "../lib/blog-data.js"; + +const author = authors[0]; + +// Kept in sync with SUBJECTS in api/public/controllers/contact.php. The server +// is authoritative — an unknown slug is coerced to "other" there — so a drift +// here degrades gracefully rather than breaking. +const subjects = [ + { value: "automation", label: "Automation" }, + { value: "website-design", label: "Website Design" }, + { value: "digital-marketing", label: "Digital Marketing" }, + { value: "fractional-cto", label: "Fractional CTO" }, + { value: "other", label: "Other" }, +]; --- -
- +
+

+ {author.jobTitle} +

+

+ Let’s build something that runs itself. +

+

+ Automation, an autonomous system, or a fractional CTO engagement — tell me what + you’re working on and I’ll get back to you. +

-
-
Contact
-

Say hello.

-

- A tip, a correction, or a question. We read everything and reply to most of it. -

-
+
+ +
+
+ + + +
-
- -
- - +
+ + +
-
- - + +
+ +
+ + +
-
- - + +
+ + +
- -
+
diff --git a/app/src/pages/index.astro b/app/src/pages/index.astro index 0b1b3eb..d8c5aaa 100644 --- a/app/src/pages/index.astro +++ b/app/src/pages/index.astro @@ -1,225 +1,249 @@ --- -import { Image } from "astro:assets"; import BaseLayout from "../layouts/BaseLayout.astro"; -import PostCard from "../components/PostCard.astro"; -import Newsletter from "../components/Newsletter.astro"; -import Icon from "../components/Icon.astro"; -import heroImage from "../assets/hero-placeholder.png"; -import { - SITE, - categories, - featuredPost, - sortedPosts, - getAuthor, - getCategory, - formatDate, - organizationSchema, - websiteSchema, -} from "../lib/blog-data.js"; +import BrandMark from "../components/BrandMark.astro"; +import InkWash from "../components/InkWash.astro"; +import IndexRow from "../components/IndexRow.astro"; +import SectionHead from "../components/SectionHead.astro"; +import { SITE, authors, personSchema } from "../lib/blog-data.js"; -const featured = await featuredPost(); -const featuredAuthor = getAuthor(featured.author); -const featuredCategory = getCategory(featured.category); -const latest = (await sortedPosts()).filter((post) => post.slug !== featured.slug); -const jsonLd = { - "@context": "https://schema.org", - "@graph": [organizationSchema(), websiteSchema()], -}; +// The theme exposes the author via `authors`, not on SITE. +const author = authors[0]; +import projects from "../config/projects.json"; +import capabilities from "../config/capabilities.json"; + +const concepts = [ + "Agentic websites", + "Autonomous SEO systems", + "Multi-agent collaboration", + "AI product architecture", + "Retrieval-augmented generation", + "AI knowledge systems", + "AI workflow orchestration", + "AI-driven software development", + "Self-healing applications", + "Autonomous business systems", +]; + +const industries = [ + "Artificial Intelligence", + "SaaS", + "FinTech", + "Healthcare", + "Legal Technology", + "Digital Marketing", + "Location Intelligence", + "Automation Platforms", +]; + +const facts = [ + { label: "Writing code since", value: "1988 — Commodore 64, age 8" }, + { label: "Professional experience", value: "26 years" }, + { label: "FinTech since", value: "2014" }, + { label: "Compliance", value: "HIPAA · SOC 2 · PCI" }, + { label: "Focused on agentic AI since", value: "2022" }, +]; + +const jsonLd = { "@context": "https://schema.org", ...personSchema(author) }; --- -
- Hero image placeholder — replace in app/src/assets -
+ + +
+
+ +
{ - categories.map((category) => ( - + capabilities.map((cap) => ( +
+

{cap.title}

+

{cap.summary}

+
)) }
+
- - -
- {latest.map((post, index) =>
- -
- - Browse the archive - + +
+
+ +
+ { + projects.map((project, i) => ( + + )) + } +
+
- + +
+
+

+ Build software that builds, improves, and manages software. +

+ The mission +
+
+ +
+
+
+

+ I’ve been writing software since I was eight years old, on a Commodore 64 in + 1988. Nearly four decades later the instinct is unchanged: if a process is + repetitive, it should build and run itself. +

+

+ For 26 years I’ve worked as a Software Engineer, CTO, Product Engineer, and + Technology Advisor across startups and enterprise — leading engineering teams + and shipping platforms in FinTech, healthcare, SaaS, digital marketing, and + geospatial technology. In FinTech since 2014, which means security and compliance + aren’t an afterthought: HIPAA, SOC 2, PCI. +

+

+ Since 2022 my focus has been singular. I don’t bolt AI onto existing + applications — I design systems where multiple agents collaborate: + researching, planning, deciding, generating content, writing software, running QA, + and optimizing continuously with minimal human intervention. +

+
+
+ { + facts.map((fact, i) => ( +
+
+ {fact.label} +
+
{fact.value}
+
+ )) + } +
+
+
+ +
+
+ +
    + { + concepts.map((concept) => ( +
  • + {concept} +
  • + )) + } +
+
+
+ +
+
+

+ Industries +

+

+ {industries.join(" · ")} +

+
+
+ +
+
+

+ Automation that actually runs itself. +

+ Start a conversation +
+
- diff --git a/app/src/pages/projects/[slug].astro b/app/src/pages/projects/[slug].astro new file mode 100644 index 0000000..f974e12 --- /dev/null +++ b/app/src/pages/projects/[slug].astro @@ -0,0 +1,369 @@ +--- +import { marked } from "marked"; +import BaseLayout from "../../layouts/BaseLayout.astro"; +import IndexRow from "../../components/IndexRow.astro"; +import { SITE } from "../../lib/blog-data.js"; +import { getProjects } from "../../lib/projects.js"; + +export async function getStaticPaths() { + const projects = await getProjects(); + return projects.map((project) => ({ + params: { slug: project.slug }, + props: { project, projects }, + })); +} + +const { project, projects } = Astro.props; + +const body = project.body ? marked.parse(project.body) : ""; + +// Everything else, so a visitor who reaches the end has somewhere to go. +const others = projects.filter((p) => p.slug !== project.slug).slice(0, 3); + +const igHandle = project.links?.instagram + ? "@" + project.links.instagram.replace(/^.*instagram\.com\//, "").replace(/\/$/, "") + : null; + +const statusLabel = { + live: "Live", + building: "In development", + archived: "Archived", + concept: "Concept", +}[project.status] ?? project.status; + +const jsonLd = { + "@context": "https://schema.org", + "@type": "CreativeWork", + name: project.title, + description: project.summary, + url: `${SITE.url}/projects/${project.slug}`, + author: { "@type": "Person", name: SITE.name, url: SITE.url }, + keywords: (project.categories ?? []).join(", "), +}; +--- + + +
+ +
+ + All projects + + +
+ {statusLabel} + { + (project.categories ?? []).map((c) => ( + + {c} + + )) + } +
+ +

+ {project.title} +

+ + { + project.lede && ( +

+ {project.lede} +

+ ) + } +
+ + +
+
+
+
Type
+
{project.kind}
+
+
+
Timeline
+
{project.period}
+
+ { + project.role && ( +
+
Role
+
{project.role}
+
+ ) + } + { + project.links?.live && ( + + ) + } + { + project.links?.instagram && ( + + ) + } + { + project.links?.github && ( + + ) + } +
+
+ + + { + (project.metrics ?? []).length > 0 && ( +
+
+ {project.metrics.map((m) => ( +
+
{m.value}
+
{m.label}
+ {m.note &&
{m.note}
} +
+ ))} +
+
+ ) + } + + + { + (project.gallery ?? []).length > 0 && ( +
+

+ Gallery +

+ +
+ ) + } + + + { + body && ( +
+
+ + + + +
+
+
+ ) + } + + + { + (project.skills ?? []).length > 0 && ( +
+

+ Skills applied +

+
+ {project.skills.map((group) => ( +
+

{group.group}

+
    + {(group.items ?? []).map((item) => ( +
  • {item}
  • + ))} +
+
+ ))} +
+
+ ) + } + + + { + (project.stack ?? []).length > 0 && ( +
+

+ Built with +

+
    + {project.stack.map((s) => ( +
  • + {s} +
  • + ))} +
+
+ ) + } + + + { + others.length > 0 && ( +
+

+ More work +

+
+ {others.map((p, i) => ( + + ))} +
+
+ ) + } + +
+

+ Building something like this? +

+ +
+
+ + + +
+ + diff --git a/app/src/pages/projects/index.astro b/app/src/pages/projects/index.astro new file mode 100644 index 0000000..51fd9bb --- /dev/null +++ b/app/src/pages/projects/index.astro @@ -0,0 +1,160 @@ +--- +import BaseLayout from "../../layouts/BaseLayout.astro"; +import { SITE, authors } from "../../lib/blog-data.js"; +import IndexRow from "../../components/IndexRow.astro"; +import { getProjects, getCategories } from "../../lib/projects.js"; + +const author = authors[0]; + +// Both come from cja_projects at build time. Filters are derived from what the +// projects actually carry, so a category can never render as an empty chip — +// tag a project "Legal" in the database and the filter appears on its own. +const projects = await getProjects(); +const categories = await getCategories(); +--- + + +
+

+ {author.jobTitle} +

+

+ Projects +

+

+ Autonomous software, AI agents, and intelligent business systems — some running in + production for years, some still being built. +

+
+ +
+ + + +
+ { + projects.map((project, i) => ( +
c.toLowerCase()) + .join(" ")} + style={`view-transition-name: proj-${project.slug}`} + > + +
+ )) + } +
+ + +
+
+ + diff --git a/app/src/pages/resume.astro b/app/src/pages/resume.astro new file mode 100644 index 0000000..fd70892 --- /dev/null +++ b/app/src/pages/resume.astro @@ -0,0 +1,228 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import SectionHead from "../components/SectionHead.astro"; +import ResumeIntro from "../components/ui/resume-intro"; +import { SITE, authors } from "../lib/blog-data.js"; +import resume from "../config/resume.json"; + +const author = authors[0]; + +const jsonLd = { + "@context": "https://schema.org", + "@type": "ProfilePage", + mainEntity: { + "@type": "Person", + name: SITE.name, + jobTitle: author.jobTitle, + email: `mailto:${resume.contact.email}`, + telephone: resume.contact.phone, + description: resume.summary, + knowsAbout: author.knowsAbout, + alumniOf: resume.education.map((e) => ({ "@type": "Organization", name: e.org })), + hasCredential: resume.certifications.map((c) => ({ + "@type": "EducationalOccupationalCredential", + name: c.title, + })), + }, +}; +--- + + + {/* client:visible — the block is the first thing on the page but the motion + only matters once it is actually in view, and this keeps the JS off the + critical path. */} + + +
+ +
+ + +
+ { + resume.experience.map((job, i) => ( +
+
+ + {job.period} + + {job.current && ( + + Now + + )} +
+ +
+

{job.role}

+

+ {job.org} + · {job.sector} +

+

+ {job.summary} +

+
    + {job.points.map((point) => ( +
  • {point}
  • + ))} +
+
+
+ )) + } +
+
+ + +
+ + +
+ { + resume.projects.map((project) => ( +
+
+

+ {project.name} +

+ + {project.kind} + + + {project.period} + +
+

+ {project.summary} +

+
    + {project.points.map((point) => ( +
  • {point}
  • + ))} +
+
+ )) + } +
+
+ + +
+ + +
+ { + resume.skills.map((group) => ( +
+

+ {group.group} +

+
    + {group.items.map((item) => ( +
  • + {item.name} + {item.years && ( + + {item.years} yr{item.years === 1 ? "" : "s"} + + )} +
  • + ))} +
+
+ )) + } +
+
+ + +
+ +
    + { + resume.tools.map((tool) => ( +
  • + {tool} +
  • + )) + } +
+
+ + +
+
+
+

+ Education +

+ { + resume.education.map((item) => ( +
+

{item.title}

+

{item.org}

+

+ {item.period} +

+
+ )) + } +
+ +
+

+ Certifications +

+ { + resume.certifications.map((item) => ( +
+

{item.title}

+

+ {item.period} +

+
+ )) + } +
+
+
+ + +
+

+ The full version, on paper. +

+ +
+
+
diff --git a/app/src/pages/services.astro b/app/src/pages/services.astro index a63523d..bcb37a8 100644 --- a/app/src/pages/services.astro +++ b/app/src/pages/services.astro @@ -1,72 +1,132 @@ --- import BaseLayout from "../layouts/BaseLayout.astro"; -import Breadcrumbs from "../components/Breadcrumbs.astro"; -import { SITE } from "../lib/blog-data.js"; +import { SITE, authors } from "../lib/blog-data.js"; +import data from "../config/services.json"; + +const author = authors[0]; +const { services, verticals, primaryVertical } = data; + +const jsonLd = { + "@context": "https://schema.org", + "@type": "Person", + name: SITE.name, + jobTitle: author.jobTitle, + url: SITE.url, + makesOffer: services.map((s) => ({ + "@type": "Offer", + itemOffered: { "@type": "Service", name: s.name, description: s.lead }, + })), +}; --- -
- + +
+

Services

+

+ Automation, websites, and marketing — built for + law firms. +

+

+ I help {primaryVertical.toLowerCase()} and service businesses win more qualified work and + follow up faster — using AI automation, custom websites, and technical marketing + that run as intelligent systems rather than manual busywork. +

+
-
-
Work with us
-

- What we offer. -

-
+ +
+ { + services.map((s, i) => ( +
+
+
+ + {String(i + 1).padStart(2, "0")} + +

+ {s.name} +

+

+ {s.lead} +

+
-
-

- This is a placeholder Services page. Describe how visitors can work with - {SITE.name}, or delete this page if you don’t need it. Edit it in - app/src/pages/services.astro. -

+
+

{s.body}

+
    + {s.points.map((p) => ( +
  • {p}
  • + ))} +
+ +
+
+
+ )) + } +
+ + +
+
+

+ Who I work with +

+ + Primarily legal +
-
-
01
-

Service one

-

- Describe your first offering here — what it is, who it’s for, and - the outcome a client can expect. -

-
+
+ { + verticals.map((v) => ( +
+

+ {v.name} + {v.primary && Primary} +

+

{v.note}

+
+ )) + } +
+
-
-
02
-

Service two

-

- Describe your second offering. Use short paragraphs and bullet lists to keep - it scannable. -

-
- -
-
03
-

Service three

-

- Describe your third offering, or remove this section if you offer fewer services. -

-
- -
-

Get in touch

-

- Everything starts with a conversation. Tell us what you’re trying to - accomplish and we’ll let you know whether we’re a good fit. -

- -
-
+ +
+

+ More qualified work, less manual work. +

+ +
diff --git a/app/src/pages/services/website-design.astro b/app/src/pages/services/website-design.astro new file mode 100644 index 0000000..1f41197 --- /dev/null +++ b/app/src/pages/services/website-design.astro @@ -0,0 +1,174 @@ +--- +import BaseLayout from "../../layouts/BaseLayout.astro"; +import { SITE } from "../../lib/blog-data.js"; +import data from "../../config/website-design.json"; + +const { eyebrow, title, lead, intro, included, process, portfolio } = data; + +const jsonLd = { + "@context": "https://schema.org", + "@type": "Service", + serviceType: "Law firm website design and development", + provider: { "@type": "Person", name: SITE.name, url: SITE.url }, + areaServed: "United States", + description: lead, +}; +--- + + + +
+ +

+ {eyebrow} +

+

+ Law firm websites that turn searches into + signed cases. +

+

+ {lead} +

+ +
+ + +
+

+ {intro} +

+
+ + +
+
+

+ Recent law firm sites +

+ + Selected work + +
+ +
+ { + portfolio.map((p) => { + const inner = ( + <> +
+ + {p.image ? ( + {`${p.name} + ) : ( + + )} +
+
+

{p.name}

+

{p.practice}

+

{p.note}

+ {p.url && ( + + {p.url.replace(/^https?:\/\//, "").replace(/\/$/, "")} ↗ + + )} +
+ + ); + + return p.url ? ( + + {inner} + + ) : ( +
{inner}
+ ); + }) + } +
+
+ + +
+

+ What’s included +

+
+ { + included.map((f) => ( +
+

{f.title}

+

{f.note}

+
+ )) + } +
+
+ + +
+

+ How it works +

+
    + { + process.map((s) => ( +
  1. + {s.step} +
    +

    {s.title}

    +

    {s.note}

    +
    +
  2. + )) + } +
+
+ + +
+

+ Ready for a site that wins cases? +

+ +
+
diff --git a/app/src/styles.css b/app/src/styles.css index b3a3640..eab0e5e 100644 --- a/app/src/styles.css +++ b/app/src/styles.css @@ -66,49 +66,191 @@ --font-mono: "JetBrains Mono", ui-monospace, monospace; } +/* ============================================================ + Carlos Arias — sumi-e brand palette + Ink on paper, vermillion as seal. + + Written as hex so these values are eyeball-verifiable against + brand/BRAND.md, which is the source of truth. Do not "improve" + a colour here — change it in the brand guide first. + + Two choices worth preserving: + · The ground is never pure white. Washi-toned. + · The near-black is blue-black, not neutral grey — real sumi + ink has a cool cast, and the whole ramp inherits it. + ============================================================ */ :root { - --radius: 0.5rem; - --background: oklch(0.985 0.004 90); - --foreground: oklch(0.18 0.01 60); - --card: oklch(0.985 0.004 90); - --card-foreground: oklch(0.18 0.01 60); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.18 0.01 60); - --primary: oklch(0.55 0.16 30); - --primary-foreground: oklch(0.99 0 0); - --secondary: oklch(0.95 0.005 90); - --secondary-foreground: oklch(0.2 0.01 60); - --muted: oklch(0.95 0.005 90); - --muted-foreground: oklch(0.5 0.01 60); - --accent: oklch(0.93 0.01 60); - --accent-foreground: oklch(0.2 0.01 60); - --destructive: oklch(0.55 0.22 27); - --destructive-foreground: oklch(0.99 0 0); - --border: oklch(0.9 0.005 80); - --input: oklch(0.9 0.005 80); - --ring: oklch(0.55 0.16 30); + --radius: 2px; /* near-square, not rounded */ + + --background: #f6f5f2; /* washi paper */ + --foreground: #14151a; /* sumi ink */ + --card: #f6f5f2; + --card-foreground: #14151a; + --popover: #efeeea; + --popover-foreground: #14151a; + + /* Primary is INK, not red. The seal is never a button ground. */ + --primary: #14151a; + --primary-foreground: #f6f5f2; + + --secondary: #efeeea; + --secondary-foreground: #3d3f47; + --muted: #efeeea; + --muted-foreground: #6e7078; + + /* Shuniku 朱肉 — seal ink. Target 1–2% coverage per screen. */ + --accent: #d0342c; + --accent-foreground: #ffffff; + + --destructive: #9b1c17; /* deeper than the seal, so + errors never read as brand */ + --destructive-foreground: #ffffff; + + --border: #dfddd7; + --input: #dfddd7; + --ring: #d0342c; + + /* ---- brand extras the base theme has no slot for ---- */ + --ca-ink-2: #3d3f47; + --ca-ink-3: #6e7078; + --ca-ink-4: #9a9ca3; + --ca-seal: #d0342c; + + /* Motion. Wabi-sabi motion is slow and never demands attention. + Nothing bounces, nothing pulses, nothing loops in the viewport. + If an effect asks to be watched, it belongs to another brand. */ + --ca-dur-quick: 180ms; + --ca-dur-base: 280ms; + --ca-dur-slow: 620ms; + --ca-dur-ink: 1500ms; + --ca-ease: cubic-bezier(0.33, 0.1, 0.25, 1); + --ca-ease-press: cubic-bezier(0.2, 0.9, 0.3, 1); + --ca-reveal-y: 12px; + --ca-stagger: 70ms; } .dark { - --background: oklch(0.16 0.005 80); - --foreground: oklch(0.95 0.005 80); - --card: oklch(0.16 0.005 80); - --card-foreground: oklch(0.95 0.005 80); - --popover: oklch(0.2 0.005 80); - --popover-foreground: oklch(0.95 0.005 80); - --primary: oklch(0.72 0.14 35); - --primary-foreground: oklch(0.16 0.005 80); - --secondary: oklch(0.22 0.005 80); - --secondary-foreground: oklch(0.95 0.005 80); - --muted: oklch(0.22 0.005 80); - --muted-foreground: oklch(0.65 0.01 80); - --accent: oklch(0.24 0.005 80); - --accent-foreground: oklch(0.95 0.005 80); - --destructive: oklch(0.7 0.19 22); - --destructive-foreground: oklch(0.16 0.005 80); - --border: oklch(0.28 0.005 80); - --input: oklch(0.28 0.005 80); - --ring: oklch(0.72 0.14 35); + --background: #121317; + --foreground: #edece8; + --card: #121317; + --card-foreground: #edece8; + --popover: #191a1f; + --popover-foreground: #edece8; + + --primary: #edece8; + --primary-foreground: #121317; + + --secondary: #191a1f; + --secondary-foreground: #c2c1bd; + --muted: #191a1f; + --muted-foreground: #8b8d95; + + /* Lifted on dark — #d0342c muddies against near-black. + Not a naive inversion. */ + --accent: #e8564a; + --accent-foreground: #121317; + + --destructive: #e8564a; + --destructive-foreground: #121317; + + --border: #2a2c33; + --input: #2a2c33; + --ring: #e8564a; + + --ca-ink-2: #c2c1bd; + --ca-ink-3: #8b8d95; + --ca-ink-4: #5e606a; + --ca-seal: #e8564a; +} + +/* Ink-surface values exposed globally, so anything that has to sit on a dark + ground (the header while the mobile menu is open, an overlay, a badge) can + reach them without hardcoding a hex or relying on being nested inside + .ca-ink-surface. Mirrors brand/tokens.css — keep the two in step. + + Note: an undefined var() invalidates the whole declaration silently, so a + missing name here shows up as "the colour just didn't apply", not an error. */ +:root { + --ca-ink-surface-bg: #121317; + --ca-ink-surface-bg-2: #191a1f; + --ca-ink-surface-rule: #2a2c33; + --ca-ink-surface-fg: #edece8; + --ca-ink-surface-fg-2: #c2c1bd; + --ca-ink-surface-fg-3: #8b8d95; + --ca-ink-surface-fg-4: #5e606a; + --ca-ink-surface-seal: #e8564a; +} + +/* ---- Ink surface ---- + A section that stays ink-ground in BOTH themes, for committed dark + moments where the content itself demands darkness. Remaps the + semantic tokens locally, so anything nested inside needs no changes. + Use sparingly — one per page at most. */ +/* IMPORTANT: this overrides the --color-* layer, NOT --background/--foreground. + The theme declares `--color-background: var(--background)` on :root, and a + custom property is substituted where it is DECLARED — so --color-background + resolved to paper at :root and only inherits from there. Redefining + --background on a descendant cannot retroactively change it, and the section + silently renders paper-on-paper. + + (.dark works only because it sits on , the same element as :root, so + the declaration re-resolves there. A nested scope has no such luck.) + + Every utility — bg-background, text-muted-foreground, border-border — reads + the --color-* names, so overriding those is both correct and sufficient. */ +.ca-ink-surface { + /* BOTH names are required, and for different consumers: + --foreground ← what Tailwind utilities compile to. + `@theme inline` emits `.text-foreground { + color: var(--foreground) }`, referencing the + underlying variable, not the alias. + --color-foreground ← what hand-written CSS in this file uses, and + what cannot be reached by overriding the raw + name alone (it was already substituted at :root). + Set one and half the section is wrong in a way that only shows up as + dark-on-dark text. Keep them in lockstep. */ + --background: #121317; + --color-background: #121317; + --foreground: #edece8; + --color-foreground: #edece8; + --card: #121317; + --color-card: #121317; + --card-foreground: #edece8; + --color-card-foreground: #edece8; + --popover: #191a1f; + --color-popover: #191a1f; + --popover-foreground: #edece8; + --color-popover-foreground: #edece8; + --primary: #edece8; + --color-primary: #edece8; + --primary-foreground: #121317; + --color-primary-foreground: #121317; + --secondary: #191a1f; + --color-secondary: #191a1f; + --secondary-foreground: #c2c1bd; + --color-secondary-foreground: #c2c1bd; + --muted: #191a1f; + --color-muted: #191a1f; + --muted-foreground: #8b8d95; + --color-muted-foreground: #8b8d95; + --accent: #e8564a; + --color-accent: #e8564a; + --accent-foreground: #121317; + --color-accent-foreground: #121317; + --border: #2a2c33; + --color-border: #2a2c33; + --input: #2a2c33; + --color-input: #2a2c33; + --ring: #e8564a; + --color-ring: #e8564a; + + --ca-ink-2: #c2c1bd; + --ca-ink-3: #8b8d95; + --ca-ink-4: #5e606a; + --ca-seal: #e8564a; + + background-color: var(--color-background); + color: var(--color-foreground); } @layer base { @@ -156,6 +298,1577 @@ } @layer components { + /* ============================================================ + Brand primitives — defined ONCE, used everywhere. + + Anything reused across more than one page belongs here, not in a + page's + + + +
+ +
+ +
+ +
+
+ +
+

+ +

+
+

Agentic AI & Automation Engineer

+

Autonomous systems that do the work teams used to do by hand.

+

I design and build AI agents and intelligent business systems that automate complex workflows end to end. 26 years as a software engineer — since 2022, focused entirely on agentic AI.

+ +
+
+ + +
+
+
+ +
+ +
+
+

What I build

+ Capabilities +
+
+

Agentic AI systems

Multi-agent architectures where AI agents collaborate to solve complex business problems end to end.

+

AI automation

Replacing repetitive business processes with intelligent, autonomous workflows.

+

Autonomous websites

Sites that continuously improve their own SEO, content, UX, and conversions using AI.

+

AI product engineering

AI-first SaaS platforms designed and shipped from concept to production.

+

Intelligent marketing systems

AI that runs technical SEO, content planning, competitor research, and optimization on its own.

+

Software architecture

APIs, cloud architecture, automation pipelines, distributed systems, and scalable backends.

+

Fractional CTO

Technology leadership for startups building AI products.

+
+
+ +
+
+

Current work

+ 2014 — present +
+ +
+ +
+ +
+
+

Build software that builds, improves, and manages software.

+ The mission +
+
+ +
+ +
+
+
+

I've been writing software since I was eight years old, on a Commodore 64 in 1988. Nearly four decades later the instinct is unchanged: if a process is repetitive, it should build and run itself.

+

For 26 years I've worked as a Software Engineer, CTO, Product Engineer, and Technology Advisor across startups and enterprise — leading engineering teams and shipping platforms in FinTech, healthcare, SaaS, digital marketing, and geospatial technology. In FinTech since 2014, which means security and compliance aren't an afterthought: HIPAA, SOC 2, PCI.

+

Since 2022 my focus has been singular. I don't bolt AI onto existing applications — I design systems where multiple agents collaborate: researching, planning, deciding, generating content, writing software, running QA, and optimizing continuously with minimal human intervention.

+
+ +
+
Writing code since
1988 — Commodore 64, age 8
+
Professional experience
26 years
+
FinTech since
2014
+
Compliance
HIPAA · SOC 2 · PCI
+
Focused on agentic AI since
2022
+
+
+
+ +
+

Research & concepts

Ongoing
+
    +
  • Agentic websites
  • +
  • Autonomous SEO systems
  • +
  • Multi-agent collaboration
  • +
  • AI product architecture
  • +
  • Retrieval-augmented generation
  • +
  • AI knowledge systems
  • +
  • AI workflow orchestration
  • +
  • AI-driven software development
  • +
  • Self-healing applications
  • +
  • Autonomous business systems
  • +
+
+ +
+

Industries

+

Artificial Intelligence · SaaS · FinTech · Healthcare · Legal Technology · Digital Marketing · Location Intelligence · Automation Platforms

+
+ +
+
+

Automation that actually runs itself.

+ hi@carlosarias.com +
+
+ + + +
+ + + + + diff --git a/site.config.json b/site.config.json index 3d0c86a..f408813 100644 --- a/site.config.json +++ b/site.config.json @@ -1,23 +1,40 @@ { - "name": "SeedProject Site", - "url": "https://example.com", - "description": "A short one-line description of this site, used for SEO and social sharing.", - "tagline": "A starter site built on SeedProject", + "name": "Carlos Arias", + "url": "https://carlosarias.co", + "description": "Agentic AI & Automation Engineer building autonomous software, AI agents, and intelligent business systems.", + "tagline": "Autonomous systems that do the work teams used to do by hand.", "language": "en", "timezone": "America/New_York", - "topic": "your subject area", - "audience": "your target audience", + "topic": "agentic AI, autonomous software, and business process automation", + "audience": "founders and CTOs who want automation, not another dashboard", "author": { - "slug": "site-author", - "name": "Site Author", - "bio": "One-line author bio — who runs this site.", - "longBio": "A longer author biography paragraph describing who runs this site and their background.", - "avatar": "/avatar-placeholder.png", - "jobTitle": "Founder & Editor", - "knowsAbout": [], - "sameAs": [] + "slug": "carlos-arias", + "name": "Carlos Arias", + "bio": "Agentic AI & Automation Engineer building autonomous software and intelligent business systems.", + "longBio": "Carlos Arias is an Agentic AI & Automation Engineer who builds autonomous software, AI agents, and intelligent business systems. He has been coding since age eight, on a Commodore 64 in 1988, and has spent 26 years as a Software Engineer, CTO, and Technology Advisor across FinTech, healthcare, SaaS, and geospatial technology. Since 2022 his focus has been entirely on agentic AI — designing multi-agent systems that research, plan, build, and optimize on their own.", + "avatar": "/carlos-arias.jpg", + "jobTitle": "Agentic AI & Automation Engineer", + "knowsAbout": [ + "Agentic AI Systems", + "Multi-Agent Collaboration", + "AI Automation", + "Autonomous Websites", + "AI Product Engineering", + "Retrieval-Augmented Generation", + "AI Workflow Orchestration", + "Software Architecture", + "Technical SEO", + "Fractional CTO" + ], + "sameAs": [ + "https://www.linkedin.com/in/hicarlosarias/", + "https://x.com/hicarlosarias", + "https://instagram.com/carlosarias.co", + "https://www.youtube.com/@hiCarlosArias", + "https://github.com/CarlosJa" + ] }, "social": { - "twitter": "" + "twitter": "@hicarlosarias" } }