All portfolio content lives in data/ as plain TypeScript modules. There is no CMS, no database, and no runtime fetching — every array is bundled at build time. The single exception is portfolio-context.ts, whose output is consumed by the chat API route at request time.
TypeScript Interfaces (types/index.ts)
A single file exports all four shared interfaces. Both data/ and components/ import from here.
import type { LucideIcon } from "lucide-react";
import type { StaticImageData } from "next/image";
export interface TimelineItem {
title: string; // Entry heading (role name or degree title)
sub_title: string; // Secondary line (company or institution)
years: string; // Date range, e.g. "2022 - 2024"
details: string; // Full description paragraph
japanese?: string; // Optional Japanese label for the entry
}
export interface Project {
title: string; // Display name
japanese: string; // Japanese subtitle shown in the card
description: string; // One-sentence description
image: StaticImageData; // Statically imported AVIF (Next.js resolves dimensions)
tags: string[]; // Technology stack labels
github: string; // GitHub repo URL ("#" if private)
demo: string; // Live demo URL
}
export interface Skill {
icon: LucideIcon; // Lucide icon component reference (not JSX element)
title: string; // English category name
japanese: string; // Japanese category label
techs: string[]; // Specific technologies in this category
}
export interface SocialLink {
image: StaticImageData; // Statically imported icon image
label: string; // Platform name, e.g. "GitHub"
href: string; // Profile URL
}
image fields in Project and SocialLink use StaticImageData — the type returned by Next.js when you import an image file directly. This lets Next.js automatically include the correct width, height, and blurDataURL without manual configuration.
data/experience.ts
Exports two TimelineItem[] arrays consumed by components/Experience.tsx via components/TimelineSection.tsx.
educations
Three entries covering formal qualifications in reverse chronological order:
| Title | Institution | Years |
|---|
| Desarrollo de Aplicaciones Web | IES Hermenegildo Lanz, Granada | 2022–2024 |
| Sistemas Microinformáticos y Redes | IES Padre Suárez, Granada | 2020–2022 |
| Certificado de Profesionalidad en SMR | Academia Hermanos Naranjo, Granada | 2018 |
experiences
Four entries covering work history in reverse chronological order:
| Title | Company | Years |
|---|
| Desarrollador Web Freelance | Freelance | 2024–Presente |
| Desarrollador Web en Prácticas | ATI Soluciones | 2024 |
| Sistemas Microinformáticos y Redes en Prácticas | Delegación Territorial de Medio Ambiente – Junta de Andalucía | 2022 |
| Técnico en SMR en Prácticas | MEGA 2 SEGURIDAD S.L | 2018 |
Connected to: Experience.tsx → TimelineSection.tsx
data/projects.ts
Exports a single Project[] array (projects) consumed by components/Works.tsx. Contains ten projects:
| Title | Japanese label | Key tags |
|---|
| Diversia Interiorismo | インテリアデザイン | NextJS, Prisma, Turso, Cloudflare |
| Recursos Web | 開発者リソース | Astro, TailwindCSS |
| Noken Study | 日本語学習 | NextJS, TailwindCSS |
| Review Forge | レビューサイト | NextJS, NestJS, Prisma, Neon |
| Noken Vocabulary | 日本語学習 | React, Supabase, React-router |
| Illustrator Amelia | イラストポートフォリオ | Astro, TailwindCSS |
| Roger Civ Dev | ポートフォリオ (Astro) | Astro, TailwindCSS |
| Portfolio Nextjs | ポートフォリオ | NextJS, TailwindCSS |
| SaaS Blog | ブログプラットフォーム | NextJS, Prisma, Stripe, Supabase |
| Premium Rental Cars | 高級車レンタル | NextJS, Prisma, Stripe, Uploadthing |
Images are statically imported at the top of the file:
import diversiaImage from "@/public/images/projects/diversia.avif";
// ...
export const projects: Project[] = [
{
title: "Diversia Interiorismo",
image: diversiaImage,
// ...
},
];
Connected to: Works.tsx
data/skills.ts
Exports a single Skill[] array (skills) consumed by components/Skills.tsx. Contains six skill categories:
| Title | Japanese | Icon | Technologies |
|---|
| Frontend | フロントエンド | CodeXml | React, TypeScript, Next.js, Astro, Tailwind CSS |
| Backend | バックエンド | Server | Node.js, Express, PHP, Laravel, REST APIs |
| Database | データベース | Database | PostgreSQL, MongoDB, MySQL, SQLite, Prisma |
| Design | デザイン | Palette | Figma, CSS, Responsive Design, UI Components |
| DevOps | デブオプス | GitBranch | Git, GitHub Actions, Docker, CI/CD |
| Tools | ツール | Wrench | VS Code, Postman, Biome, pnpm, Vite |
All icons are from the lucide-react package. The icon field stores the component reference itself, not a rendered element:
import { CodeXml, Database } from "lucide-react";
export const skills: Skill[] = [
{
icon: CodeXml, // Component reference, not <CodeXml />
title: "Frontend",
japanese: "フロントエンド",
techs: ["React", "TypeScript", "Next.js", "Astro", "Tailwind CSS"],
},
];
Inside Skills.tsx, each icon is instantiated as:
const Icon = skill.icon;
return <Icon className="w-7 h-7 text-primary" />;
Connected to: Skills.tsx
data/socials.ts
Exports a single SocialLink[] array (socialLinks) with three entries. Images are statically imported AVIF files from public/images/socials/.
export const socialLinks: SocialLink[] = [
{
image: githubImage,
label: "GitHub",
href: "https://github.com/rogerciv",
},
{
image: linkedinImage,
label: "LinkedIn",
href: "https://linkedin.com/in/rogerciv",
},
{
image: instagramImage,
label: "Instagram",
href: "https://instagram.com/tuusuario",
},
];
Connected to: portfolio-context.ts (used to build the AI system prompt; not rendered directly by a UI component).
data/socials.ts feeds only the AI assistant context. The Footer component manages its own local social link data (Links[] type, PNG sources from public/images/footer/) independently.
data/portfolio-context.ts
Exports a single function generatePortfolioContext(): string that builds the system prompt string used by the chat API route.
Imports from: experience.ts, projects.ts, skills.ts, socials.ts
Consumed by: app/api/chat/route.ts
export function generatePortfolioContext(): string {
const skillsText = skills
.map((skill) => `- ${skill.title}: ${skill.techs.join(", ")}`)
.join("\n");
const projectsText = projects
.map((p) => `- ${p.title}: ${p.description} (Tecnologías: ${p.tags.join(", ")}). Demo: ${p.demo}`)
.join("\n");
// ... similarly for experiences, educations, socialLinks
return `
Eres el asistente personal de Roger Civ, desarrollador web full stack.
...
HABILIDADES TÉCNICAS
${skillsText}
EXPERIENCIA
${experienceText}
...
`;
}
The returned string serves as the system field in the Groq API call. It includes:
| Section | Source |
|---|
| Personal bio and values | Hardcoded in the template literal |
| Technical skills | data/skills.ts |
| Work experience | data/experience.ts (experiences) |
| Education | data/experience.ts (educations) |
| Projects | data/projects.ts |
| Social links | data/socials.ts |
| Assistant behavior rules | Hardcoded in the template literal |
The API route caches the output of this function for 5 minutes to avoid regenerating the string on every request:
let cachedContext: string | null = null;
let lastContextUpdate = 0;
const CONTEXT_CACHE_DURATION = 5 * 60 * 1000;
function getPortfolioContext() {
const now = Date.now();
if (!cachedContext || now - lastContextUpdate > CONTEXT_CACHE_DURATION) {
cachedContext = generatePortfolioContext();
lastContextUpdate = now;
}
return cachedContext;
}
Data-to-Component Map
| Data file | Exported symbol | Consumed by |
|---|
experience.ts | educations: TimelineItem[] | Experience.tsx → TimelineSection.tsx |
experience.ts | experiences: TimelineItem[] | Experience.tsx → TimelineSection.tsx |
projects.ts | projects: Project[] | Works.tsx |
skills.ts | skills: Skill[] | Skills.tsx |
socials.ts | socialLinks: SocialLink[] | portfolio-context.ts |
portfolio-context.ts | generatePortfolioContext() | app/api/chat/route.ts |
Project Structure Overview
Directory tree, App Router setup, and data flow diagram.
Components
Props and implementation details for every React component.