Overview
Portfolio Javier Navas offers a unique, macOS-inspired desktop experience directly in your browser. Built with React Router 7, it combines modern web technologies with a nostalgic desktop interface.
All windows are fully draggable, resizable, and support minimize/maximize operations just like a real desktop OS.
Core Features
Interactive Desktop Full macOS-style desktop environment with drag-and-drop windows
Terminal Emulator Fully functional terminal with file system navigation and custom commands
Calendar System Event calendar with company filtering and detailed project views
Photo Gallery Grid-based photo viewer with preview panel
Window Management Minimize, maximize, close, and drag windows anywhere
File Browser Finder-style interface for browsing projects, experience, and studies
Desktop Environment
The desktop component (app/components/Desktop.tsx) is the heart of the application, managing all windows and state.
The top menu bar displays:
Portfolio owner’s name (“JN” logo)
System time (optional)
Apple-style menu options
Desktop Icons
Four folders are displayed on the left side of the desktop:
const folders = [
{ id: "proyectos" , label: "Proyectos" , icon: "📁" },
{ id: "experiencia" , label: "Experiencia" , icon: "💼" },
{ id: "estudios" , label: "Estudios" , icon: "🎓" },
{ id: "fotos" , label: "Fotos" , icon: "📷" },
];
Double-click any folder to open its window.
Dock
The bottom dock provides quick access to all applications:
Terminal 🖥️ Command-line interface
Proyectos 📁 Projects folder
Experiencia 💼 Work experience
Estudios 🎓 Education history
Calendario 📅 Event calendar
Icons in the dock show a subtle glow effect when their window is open.
Terminal Emulator
The terminal (app/components/Terminal.tsx) is a fully functional command-line interface with:
File System Navigation
Navigate through a virtual file system structure:
~
├── proyectos/
│ ├── Arcadiax.txt
│ ├── Automatizacion_Francia.txt
│ └── Pipeline_PositionHoldings.txt
├── trabajos/
│ ├── Everis.txt
│ ├── Inetum.txt
│ └── NFQ.txt
└── estudios/
├── grado-informatica.txt
└── DAM.txt
Command Set
Example terminal session:
[email protected] :~$ help
Comandos disponibles:
─────────────────────────────────────────────────
about → Sobre mí
whoami → Quién soy
ls / dir → Listar directorio actual
cd < carpet a > → Entrar en una carpeta
cd .. → Volver atrás
cat < archivo.tx t > → Leer un archivo
open cv.pdf → Abrir / descargar el CV
ping < proyect o > → Info + URL de un proyecto
git init → Mi cuenta de GitHub
clear → Limpiar terminal
[email protected] :~$ cd proyectos
[email protected] :~/proyectos$ ls
Arcadiax.txt Automatizacion_Francia.txt Pipeline_PositionHoldings.txt
[email protected] :~/proyectos$ cat Arcadiax.txt
Arcadiax
─────────────────────────────────────────────────
Descripción: ArcadiaX es un ecosistema tecnológico personal...
Tech: React · TypeScript · Node.js
URL: https://proyecto1.com
Tab Completion
Press Tab to autocomplete commands, file names, and folder names:
function getCompletions ( raw : string ) : string [] {
const parts = raw . split ( / \s + / );
// Command completion
if ( parts . length === 1 ) {
return COMMANDS . filter (( c ) => c . startsWith ( parts [ 0 ]));
}
// File/folder completion based on current command
// ...
}
Command History
Use ↑ and ↓ arrow keys to navigate through previous commands.
The terminal is read-only - you cannot modify the actual file system. It’s a simulated environment for portfolio presentation.
Window System
The window component (app/components/Window.tsx) provides a complete window management system.
Window Controls
Each window has three macOS-style buttons:
🔴 Red - Close window
🟡 Yellow - Minimize to dock
🟢 Green - Maximize/restore window
Draggable Windows
Windows use react-draggable for smooth drag operations:
import Draggable from "react-draggable" ;
< Draggable
nodeRef = { nodeRef }
handle = ".win-titlebar"
cancel = ".win-btn"
defaultPosition = { defaultPosition }
bounds = { { top: 30 , left: 0 } }
>
{ windowContent }
</ Draggable >
Z-Index Management
Windows automatically come to front when clicked:
const bringToFront = useCallback (( id : WinId ) => {
topZRef . current += 1 ;
const z = topZRef . current ;
setWindows (( ws ) => ws . map (( w ) =>
w . id === id ? { ... w , zIndex: z } : w
));
}, []);
Window State
Each window maintains its state:
interface WinState {
id : WinId ;
title : string ;
isOpen : boolean ;
isMinimized : boolean ;
zIndex : number ;
defaultPosition : { x : number ; y : number };
width : number ;
height : number ;
}
Finder-Style Browser
The file browser provides a macOS Finder-like experience for navigating content.
< div className = "flex h-full" >
{ /* Sidebar */ }
< div className = "w-36 shrink-0 bg-[#1a1a1a]" >
< p className = "text-gray-500" > Favoritos </ p >
{ allFolders . map (( f ) => (
< button onClick = { () => setActive ( f . key ) } >
< span > { f . icon } </ span >
< span > { f . label } </ span >
</ button >
)) }
</ div >
{ /* Files grid */ }
< div className = "flex-1 bg-[#1e1e1e]" >
{ current . items . map (( item ) => (
< TxtFileIcon name = { item . name } onOpen = { item . onOpen } />
)) }
</ div >
</ div >
File Icons
Double-click any .txt file to open it in a new document window:
function TxtFileIcon ({ name , onOpen } : { name : string ; onOpen : () => void }) {
return (
< div onDoubleClick = { onOpen } className = "group" >
< span className = "text-5xl" > 📄 </ span >
< span className = "text-xs" > { name } .txt </ span >
</ div >
);
}
Document Viewer
Opened documents display formatted content:
function ProyectoDoc ({ p } : { p : Proyecto }) {
return (
< DocView >
< p className = "text-yellow-400 font-bold" > { p . name } </ p >
< p className = "text-gray-500" > ~/proyectos/ { p . slug } .txt </ p >
< div className = "border-t border-white/10 pt-4 space-y-4" >
< div >
< p className = "text-cyan-400" > Descripción </ p >
< p className = "text-gray-300" > { p . description } </ p >
</ div >
< div >
< p className = "text-cyan-400" > Tecnologías </ p >
< div className = "flex gap-2" >
{ p . tech . map (( t ) => (
< span key = { t } className = "badge" > { t } </ span >
)) }
</ div >
</ div >
</ div >
</ DocView >
);
}
Calendar System
The calendar (app/components/Calendar.tsx) provides a comprehensive event tracking system.
Company Filtering
Filter events by company with color-coded categories:
const calendarCompanies = [
{ key: "everis" , label: "Everis" , color: "#07f1b7" },
{ key: "inetum" , label: "Inetum" , color: "#818cf8" },
{ key: "nfq" , label: "NFQ" , color: "#c084fc" },
];
Event Structure
Each event contains detailed information:
interface CalEvent {
id : string ;
title : string ;
category : CalCategory ;
problema : string ; // Problem statement
solucion : string ; // Solution implemented
aprendizaje : string ; // Lessons learned
start : Date ;
end ?: Date ;
color : string ;
}
Interactive Calendar Grid
Month navigation (previous/next)
“Today” quick button
Click any day to view events
Color-coded event indicators
Sidebar detail panel
MongoDB Integration
Dynamic events can be loaded from MongoDB:
export async function loader () {
try {
const nfqEvents = await getNfqEvents ();
console . log ( `[MongoDB] ${ nfqEvents . length } eventos NFQ cargados` );
return { nfqEvents };
} catch ( e ) {
console . error ( "[MongoDB] Error:" , ( e as Error ). message );
return { nfqEvents: [] };
}
}
From app/lib/mongodb.server.ts:
export async function getNfqEvents () : Promise < NfqEventRaw []> {
const client = await getClient ();
const db = process . env . MONGODB_DB ?? "portafolio" ;
const col = process . env . MONGODB_COLLECTION_NFQ ?? "nfq" ;
const docs = await client
. db ( db )
. collection ( col )
. find ({})
. sort ({ fecha: 1 })
. toArray ();
return docs . map (( doc ) => ({
id: doc . _id . toString (),
title: doc . titulo as string ,
category: "nfq" as const ,
problema: doc . problema as string ,
solucion: doc . solucion as string ,
aprendizaje: doc . aprendizaje as string ,
start: new Date ( doc . fecha as string | Date ). toISOString (),
color: "#c084fc" ,
}));
}
The calendar merges static events from textos.ts with dynamic MongoDB events, giving you flexibility in content management.
Photo Gallery
The photo gallery (app/components/PhotoGallery.tsx) provides an elegant image viewing experience.
Grid Layout
const FOTOS : string [] = [
"img1.jpg" ,
"img2.jpg" ,
"img3.jpg" ,
"img4.jpg" ,
"img5.JPG" ,
];
return (
< div className = "h-full flex bg-[#111]" >
{ /* Grid */ }
< div className = "flex-1 overflow-auto p-4" >
< div className = "grid grid-cols-3 gap-2" >
{ FOTOS . map (( foto ) => (
< button
key = { foto }
onClick = { () => setSelected ( foto ) }
onDoubleClick = { () => onOpenPhoto ( foto ) }
className = "aspect-square overflow-hidden rounded-lg"
>
< img
src = { `/fotos/ ${ foto } ` }
alt = { foto }
className = "w-full h-full object-cover"
/>
</ button >
)) }
</ div >
</ div >
{ /* Preview panel */ }
{ selected && (
< div className = "w-56 shrink-0 border-l" >
< img src = { `/fotos/ ${ selected } ` } />
< button onClick = { () => onOpenPhoto ( selected ) } >
Abrir en visor
</ button >
</ div >
) }
</ div >
);
Features
3-column responsive grid
Click to select photo
Double-click to open in full viewer
Preview panel on the right
Smooth transitions and hover effects
Add your photos to the public/fotos/ directory and update the FOTOS array in PhotoGallery.tsx.
Content Management
All portfolio content is centralized in app/textos.ts:
export const textos = {
meta: {
title: "Portfolio · Javier Navas" ,
description: "Portfolio personal de Javier Navas..." ,
},
terminal: {
prompt: "[email protected] " ,
about: [
"Hola, soy Javier Navas." ,
"Ingeniero Informático apasionado por..." ,
],
proyectos: [
{
slug: "Arcadiax" ,
name: "Arcadiax" ,
description: "ArcadiaX es un ecosistema tecnológico..." ,
tech: [ "React" , "TypeScript" , "Node.js" ],
url: "https://proyecto1.com" ,
},
// More projects...
],
trabajos: [
{
slug: "NFQ" ,
empresa: "NFQ" ,
rol: "Consultor en banca" ,
periodo: "Octubre 2025 - Actual" ,
descripcion: "En NFQ Advisory Solutions trabajo en..." ,
},
// More jobs...
],
estudios: [
{
slug: "grado-informatica" ,
titulo: "Grado en Ingeniería Informática" ,
centro: "En la universidad he adquirido..." ,
periodo: "2018 – 2026" ,
},
// More education...
],
},
};
Responsive Design
The portfolio is fully responsive:
Desktop (768px and above): Full window management system
Mobile (below 768px): Simplified single-window view
Touch Support : Mobile devices can interact with the terminal
useEffect (() => {
if ( window . innerWidth >= 768 ) inputRef . current ?. focus ();
}, []);
Technology Stack
React Router 7 Full-stack framework with SSR
React 19 Latest React with concurrent features
TailwindCSS 4 Utility-first CSS framework
TypeScript Type-safe development
react-draggable Drag-and-drop functionality
MongoDB Optional database integration
Server-Side Rendering
Enabled by default in react-router.config.ts:
export default {
ssr: true ,
} satisfies Config ;
Code Splitting
React Router automatically splits routes for optimal loading.
Asset Optimization
Vite handles:
CSS minification
JavaScript bundling
Image optimization
Tree shaking
Lazy Loading
Dynamic imports for heavy components:
const PhotoViewer = lazy (() => import ( './PhotoViewer' ));
Customization Options
Theme Colors
Brand colors are used throughout:
Primary : #07f1b7 (Teal)
Light : #818cf8 (Indigo)
Dark : #c084fc (Purple)
Window Appearance
const INITIAL_WINDOWS : WinState [] = [
{
id: "terminal" ,
title: "[email protected] — terminal" ,
defaultPosition: { x: 140 , y: 50 },
width: 720 ,
height: 500 ,
},
// Customize positions and sizes...
];
ASCII Art
Customize the terminal banner in Terminal.tsx:
const ASCII = `
██╗ █████╗ ██╗ ██╗██╗███████╗██████╗
// Your custom ASCII art...
` ;
Next Steps
Customize Content Learn how to personalize the portfolio for your own use
Component Reference Detailed API documentation for each component
Deploy Deploy your portfolio to production
Terminal Data Customize terminal commands and content