Perfil Servicios Tools Blog Contactar
9 AI Gadgets, Zero Frameworks: A Vanilla JS Portfolio Architecture

9 AI Gadgets, Zero Frameworks: A Vanilla JS Portfolio Architecture

23,000 lines of code, 9 working prototypes, 0 frameworks. OCR, NLP, 3D, CRM, weather, all in plain JavaScript. Here's how it's organized and why no framework won.

The Decision: Why No Frameworks?

It wasn’t anti-React ideology. It was a practical call: I wanted to prove I understand the underlying technology, not just that I know how to install npm packages. Anyone can run npx create-react-app. But building a component system, state management, lazy loading and i18n from scratch demonstrates something different — it shows you understand what a framework is actually doing for you underneath the API.

There’s also a concrete performance argument. For a static portfolio deployed on Cloudflare Pages, a framework adds unnecessary overhead: build steps, a node_modules dependency tree, bundling time. My site loads in under 2 seconds because there’s no Virtual DOM to reconcile on first paint — it’s HTML shipped straight to the browser, styles already resolved, no hydration step waiting to kick in before the page feels interactive.

That’s not a knock on frameworks in general — for a team shipping a product with dozens of contributors, React or Vue earn their overhead many times over through maintainability and hiring pool size. But for a single-author portfolio whose entire purpose is to demonstrate what I can build with my own hands, vanilla JavaScript was the more honest choice, and it happens to also be the faster one.

There’s a second, less obvious reason: longevity. A vanilla JS site built in 2024 still runs today without a single dependency update, security advisory, or breaking major-version bump to worry about. No npm audit warnings, no framework migration guide to follow when a new major version drops, no lockfile to keep in sync. The tradeoff is that I write more boilerplate by hand than a framework would generate for me — but that boilerplate doesn’t rot, and I never have to relearn my own codebase after a framework’s API changes underneath it.

The Map of 23,000 Lines

File Lines Function
solar-system-pro.js 8,087 Three.js astronomical simulator + Launch Library 2 API
crm-studio-pro.js 2,850 CRM with pipeline and analytics
weather-pro.js 1,950 Weather dashboard
neural-bricks-3d.js 1,800 Three.js voxel engine
gadget-popups.css 1,685 Gadget-expand system
sentiment-analyzer-pro.js 1,600 NLP with scoring
pricing-simulator-pro.js 1,400 Dynamic ML pricing
ocr-scanner-pro.js 1,350 OCR with Tesseract.js
script.js 1,315 Core + boot screen
email-pro.js 1,100 Professional email rewriting
styles.css 2,007 Main stylesheet
Other (i18n, info, popups) ~1,800 Infrastructure

Nine interactive gadgets, roughly 23,000 lines of hand-written JavaScript and CSS, zero runtime dependencies. No React, no Vue, no jQuery, no Lodash. Every one of those numbers is a file I can open right now and explain line by line, which is exactly the point of building it this way.

The Shared Pattern: The “Gadget Expand”

Every gadget follows the same pattern: a compact card in the portfolio grid (card mode) and an expanded lab mode that takes over the full screen. The expand system is an overlay managed by gadget-popups.js, the hub every single gadget talks to:

function openGadgetExpand(gadgetId) {
    const backdrop = document.getElementById('gadget-expand-backdrop');
    const content = document.getElementById('gadget-expand-content');

    // Lazy init: only build the gadget's DOM when it's opened
    if (!content.dataset.initialized) {
        content.innerHTML = buildGadgetHTML(gadgetId);
        initGadgetLogic(gadgetId);
        content.dataset.initialized = gadgetId;
    }

    backdrop.classList.add('active');
    document.body.style.overflow = 'hidden';

    // Trap focus inside the modal for accessibility
    trapFocus(content);
}

The lazy init is the piece that matters most: heavy gadgets (Solar System, Neural Bricks) don’t spin up their Three.js scenes until the user actually opens them. That’s what keeps the home page fast even though it has nine gadgets registered — a visitor who never opens the 3D simulators never pays their initialization cost, and the browser never allocates a WebGL context it doesn’t need.

The dataset.initialized check is doing something subtle too: it stores the gadget’s own ID, not just a boolean. That means if the backdrop is reused for a different gadget later in the session, the check correctly detects a mismatch and rebuilds the content instead of showing stale DOM from whichever gadget opened first. It’s a one-line guard, but without it the second gadget a visitor opens in a session would silently render the first one’s leftover markup — a bug that’s easy to miss in testing because it only shows up on the second interaction, not the first.

Closing a gadget runs the mirror operation: each gadget’s destroy() function tears down its event listeners, cancels any requestAnimationFrame loop still running, and — for the Three.js gadgets — disposes geometries, materials and the WebGL context itself. Skipping that step is the single most common way a page like this leaks memory: open and close the Solar System gadget a dozen times without proper disposal and the tab’s memory footprint climbs steadily until the browser starts stuttering.

Gadget by Gadget: Technical Decisions

1. OCR Scanner Pro — Tesseract.js

The scanner uses Tesseract.js v4 for real text extraction in the browser. The main challenge is performance: WebAssembly-based OCR takes 3-8 seconds depending on the image. The solution was a pipeline with visual feedback so the wait never feels dead:

async function processImage(imageData) {
    updateProgress('Initializing Tesseract...', 10);
    const worker = await Tesseract.createWorker('spa+eng');

    updateProgress('Analyzing image...', 30);
    const { data } = await worker.recognize(imageData, {
        logger: m => {
            if (m.status === 'recognizing text') {
                updateProgress('Recognizing text...', 30 + m.progress * 60);
            }
        }
    });

    updateProgress('Processing results...', 95);
    displayResults(data);
    await worker.terminate();
}

Running dual-language recognition (spa+eng) roughly doubles the model-loading cost compared to a single language, but it means the scanner handles both Spanish and English documents out of the box without asking the user to pick a language up front — a small UX win that’s worth the extra load time.

2. Sentiment Analyzer — Client-Side NLP

The sentiment analyzer works with no external API. It uses a dictionary of roughly 2,000 words scored for sentiment (positive/negative/neutral), plus heuristics to catch negation, intensifiers, and emoji. It’s not GPT-4, but it proves the concept of NLP applied entirely client-side, with zero network round-trips and zero per-request cost — which matters for a portfolio gadget that has to survive unlimited free usage from strangers on the internet.

3. Dynamic Pricing — Multi-Factor Algorithm

The pricing simulator computes optimal prices by combining four factors (demand, competition, seasonality, stock) with configurable weights. It includes an advanced mode with sensitivity curves and what-if analysis:

function calculateOptimalPrice(base, factors) {
    const demandMultiplier = 1 + (factors.demand - 50) / 100 * 0.8;
    const competitionFactor = 1 - (factors.competition - 50) / 100 * 0.3;
    const seasonBoost = 1 + Math.sin(factors.season * Math.PI / 100) * 0.4;
    const stockPressure = 1 + (100 - factors.stock) / 100 * 0.5;

    return base * demandMultiplier * competitionFactor
               * seasonBoost * stockPressure;
}

Each factor is centered at 50 so that a “neutral” input leaves the base price untouched, and each has its own weight tuned so no single slider can push the price into an unrealistic range on its own — you have to combine extremes across several factors to see a dramatic swing, which mirrors how real pricing pressure actually compounds.

4. Weather Dashboard — Open-Meteo API

The weather widget consumes the free Open-Meteo API (no API key required). It first geocodes the city, then fetches a 7-day forecast. The expanded mode shows temperature, precipitation and wind charts built with plain Canvas — no Chart.js, no D3, no charting library at all.

5. CRM Studio Pro — Complex State Without Redux

This is the gadget that most resembles a “real app”: a Kanban pipeline with drag & drop, contact forms, analytics with charts, and CSV/JSON export. All of the state lives in a plain JavaScript object in memory, with optional persistence to localStorage. No Redux, no Zustand, no reducers — just a single source-of-truth object and a set of functions that mutate it and re-render the affected DOM nodes.

6-7. Neural Bricks + Solar System — Three.js

Both 3D gadgets use Three.js r128 but with completely different approaches. Neural Bricks is a voxel engine with 8 procedural models and frame-by-frame animation. Solar System is a scientific simulation built on real NASA data. I wrote a full technical breakdown of the Solar System gadget here.

8. Before/After Slider — Canvas + CSS Filters

The visual comparison tool looks simple but has real depth underneath: a circular lens mode, crossfade, 12 real-time CSS filters, fullscreen mode, and screenshot export. All of it built with the Canvas API and direct pixel manipulation, no image-processing library involved.

The i18n System

The portfolio is bilingual (ES/EN) with a homegrown internationalization system. 195 translation keys managed through data-i18n attributes in the HTML:

// i18n.js — 324 lines
const I18N = {
    dict: {
        'nav.profile':  { es: 'Perfil',    en: 'Profile' },
        'nav.services': { es: 'Servicios', en: 'Services' },
        // ... 193 more
    },

    _apply() {
        document.querySelectorAll('[data-i18n]').forEach(el => {
            const val = this.t(el.getAttribute('data-i18n'));
            val.includes('<') ? el.innerHTML = val : el.textContent = val;
        });
        // Also updates placeholders, aria-labels, <html lang>
    }
};

The whole system is under 350 lines and has no runtime dependency on anything beyond querySelectorAll. Switching languages doesn’t reload the page — it walks the DOM once, swaps every tagged string, flips the persisted preference in localStorage, and updates <html lang> for accessibility tools and search engines in the same pass.

The part that took longer than the code itself was the discipline around it: every string that goes into data-i18n also has to exist, word for word, as the fallback text sitting inside that same HTML element. That fallback is what a visitor sees for the split second before the JavaScript bundle finishes parsing and _apply() runs — and it’s what a search engine crawler sees if it doesn’t execute JavaScript at all. Losing that discipline even once means a visitor on a slow connection briefly sees the wrong language, or a crawler indexes a page with Spanish fallback text under an English URL. It’s not a hard rule to follow, but it’s an easy one to forget under deadline pressure, which is exactly when it causes the most damage.

Boot Screen: The First Impression

The boot screen simulates a Linux terminal with matrix rain, loads ASCII art of the logo, and types out “system” lines one character at a time. It’s pure theater, but it serves a real function: preloading assets while the user is entertained. By the time the sequence finishes, fonts, images and scripts are already cached, so the actual portfolio content that follows renders instantly instead of popping in piece by piece.

What I’d Do Differently

With 23K lines of vanilla JS, there are things a framework would genuinely do better:

But for a portfolio whose entire purpose is demonstrating technical capability, vanilla JS was the right call. It shows I understand what frameworks do under the hood, and that I can build without them when the situation calls for it.

The best framework for a portfolio is no framework. It proves you know how to program, not that you know how to install dependencies.

All nine gadgets are live at cristiancorrales.com. Open any of them and hit “LABORATORIO” to see the full mode.

Building something similar?

I build AI integrations, SEO systems and 3D/web experiments for companies. Based in Spain, working remotely with teams anywhere.

Get in touch →

All articles in English → · Prefer Spanish? Read the original article in Spanish →

Share this article

Cristian Corrales

Cristian Corrales

AI Solutions Architect based in Calafell (Tarragona, Spain). I build AI, SEO and automation systems for businesses, plus open experiments with Three.js and vanilla JavaScript.