// full-stack web designer
Ioannis Kaperdas.
I design and build high-performance digital products, web applications, and brand websites using Next.js, React, and TypeScript—from strategy and wireframes to production deployment.
available for new projectsathens, greece · remote worldwide
Things I've built
A few recent projects — the problem, what I shipped, and what changed.
Non-profit digital archive · 2026
Lost Hotels of Greece
- problem
- Greece's 20th-century grand hotels — Belle Époque seafront palaces, interwar landmarks, the modernist state Xenia network — are being demolished or left to decay. Their histories are fragmented across unreliable, increasingly AI-muddied sources, with no map-based way for the public to explore them.
- built
- An independent, source-verified, bilingual (EL/EN) archive in React 19 + TypeScript + Vite. Explorable by interactive Leaflet map, architectural style, region and a chronological lifespan timeline — with an AI “historic archivist” (Groq / Llama 3.3) grounded in the cited record, not invented detail.
- result
- A single, museum-grade home for 12 documented lost hotels and growing — every claim traceable to reputable sources, every image public-domain or CC with full attribution.
12 verified hotels
- React 19
- TypeScript
- Leaflet
- Groq AI
- EL / EN
AI compliance console · 2026
Infinite Impressions × Cookies
- problem
- Cookie-consent banners sit in a no-win zone: privacy law (GDPR, CPRA, Consent Mode v2) demands fair, transparent consent while teams chase opt-ins — so the web is full of dark patterns, trackers firing before consent, and inaccessible banners. Most teams can't even tell if theirs is compliant.
- built
- An AI console that turns that uncertainty into action across five modules: a Trust Auditor that scores any URL and emits copy-pasteable code fixes, an AI banner redesigner with live preview and React/HTML export, a script-load timeline inspector, regional consent analytics, and a privacy consultant tuned on EDPB/CNIL/CCPA guidance. React 19 + Vite + Tailwind v4.
- result
- A stark, editorial, WCAG-AA console with real Recharts data-viz — and a JS bundle trimmed from ~1057 kB to ~216 kB along the way.
1057 → 216 kB
- React 19
- Vite
- Tailwind v4
- Recharts
- AI
Explainable spam ML · 2026
MailSentinel
- problem
- Spam and phishing tools are black boxes — most “detectors” are hand-written if/else rules with no honest metrics, demos make you log into Gmail before you see anything, and the public spam corpora are 20 years old.
- built
- A real classifier (TF-IDF + logistic regression, 97.7% accuracy, 0.997 ROC-AUC) built as a glass box: each word's weight drives inline red/green highlighting and a ranked “why” panel. Inference runs client-side — pasted email never leaves the device — with a Python training pipeline and a parity test matching the TypeScript inference to machine epsilon. Next.js 16 + TypeScript.
- result
- A login-free, privacy-by-design demo you try in one click, with an open model card and modern-mail domain adaptation so today's email scores correctly. Optional read-only Gmail/Outlook OAuth.
97.7% accuracy
- Next.js 16
- TypeScript
- scikit-learn
- Explainable ML
Code I'm working on lately
Patterns I reach for on real work — each solves a problem you'll recognise. Pick one to read it.
Modal.tsx
Modal dialogs that trap focus and restore it — without a UI library.
// Problem: accessible modal dialogs that trap focus and restore it on close,
// without pulling in a whole UI library.
import { useEffect, useRef } from "react";
export function Modal({ open, onClose, title, children }: {
open: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const previouslyFocused = document.activeElement as HTMLElement | null;
const focusables = ref.current!.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled]), input, [tabindex]:not([tabindex="-1"])',
);
focusables[0]?.focus();
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") return onClose();
if (e.key !== "Tab" || focusables.length === 0) return;
const first = focusables[0];
const last = focusables[focusables.length - 1];
// Wrap focus so Tab never escapes the dialog.
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
previouslyFocused?.focus(); // restore focus to the trigger element
};
}, [open, onClose]);
if (!open) return null;
return (
<div className="overlay" onClick={onClose}>
<div
ref={ref}
role="dialog"
aria-modal="true"
aria-label={title}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
);
}I build the whole thing
I'm Ioannis Kaperdas, a freelance web developer with an MSc in IT. I design and build websites and web applications end to end — from the first wireframe to a production deploy.
I care about the things that make software actually good: fast load times, accessible markup, typed and maintainable code, and clear communication along the way — not jargon.
Tools are means to an end. I pick the right ones for the job and keep the codebase something the next person (often you) can live with.
what I do
- Web apps & dashboards
- /Marketing & landing sites
- /Performance & SEO
- /Design → code
- based in
- Athens, Greece · Remote worldwide
- stack
- TypeScript · React · Next.js
- working with
- Startups, agencies & founders
// toolkit
- TypeScript
- React
- Next.js
- Node.js
- Tailwind CSS
- PostgreSQL
- REST & GraphQL
- Figma → Code
- Web Vitals
- Vercel / CI
Let's work together
Have something in mind, or just want to chat? Tell me about it — I'll reply within a day.

