/* Public portfolio pages */
/* Project Detail Page */
function ProjectDetail({ project, onClose, tweaks }) {
const primary = tweaks.primaryColor;
const [backHovered, setBackHovered] = React.useState(false);
return (
setBackHovered(true)}
onMouseLeave={() => setBackHovered(false)}
style={{
display: 'inline-flex', alignItems: 'center', gap: 10,
padding: '10px 18px', borderRadius: 14,
border: `1px solid ${backHovered ? primary + '73' : primary + '42'}`,
background: backHovered
? `linear-gradient(180deg, ${primary}18, rgba(var(--theme-surface-rgb),0.52))`
: 'rgba(var(--theme-surface-rgb),0.38)',
color: primary,
fontFamily: 'Poppins, sans-serif',
fontWeight: 700, fontSize: 14, cursor: 'pointer',
marginBottom: 32,
transition: 'background 0.24s ease, border-color 0.24s ease, color 0.24s ease, transform 0.24s ease, box-shadow 0.24s ease',
transform: backHovered ? 'translateX(-3px)' : 'translateX(0)',
boxShadow: backHovered ? `0 14px 30px rgba(0,0,0,0.28), 0 0 0 1px ${primary}14` : 'none',
}}>
Back to Projects
{project.category}
{project.title}
{project.summary}
{[
['Role', project.role],
['Timeline', project.duration],
['Impact', project.impact],
].map(([label, value]) => (
))}
);
}
function DetailSection({ title, color, children }) {
return (
);
}
function DetailList({ items, color }) {
return (
{items.map((item, i) => (
))}
);
}
/* Hero Section */
function HeroSection({ onNavigate, tweaks }) {
const primary = tweaks.primaryColor;
return (
{/* Decorative elements */}
{/* Gradient orb */}
Hello, I'm Ethan
DATAANALYST
I work with data to understand what is really happening, then turn those findings into reports, dashboards, and practical tools that help teams make better decisions.
{/* Scroll indicator */}
onNavigate('about')} style={{
position: 'absolute', bottom: 40, left: '50%', transform: 'translateX(-50%)',
width: 44, height: 44, borderRadius: '50%',
border: '1px solid rgba(var(--theme-text-rgb),0.12)',
background: 'rgba(var(--theme-surface-rgb),0.18)',
color: 'var(--theme-text)',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
animation: 'bounce 2s infinite', opacity: 0.68, cursor: 'pointer',
zIndex: 2, transition: 'opacity 0.25s ease, border-color 0.25s ease, background 0.25s ease',
}}>
);
}
/* About Section */
function AboutSection({ tweaks }) {
const primary = tweaks.primaryColor;
return (
About me
I'm a Data Analyst with a strong background in business intelligence, data engineering, and predictive analytics. Previously worked as an ERP Implementation Analyst, where I gained deep expertise in enterprise systems and data workflows.
I specialize in turning raw data into strategic insights, building automated pipelines, and creating dashboards that empower decision-makers at every level.
);
}
function TechChip({ tech }) {
return (
{tech.mark}
{tech.label}
);
}
function TechMarquee({ tweaks }) {
const row1 = [
{ label: 'Python', mark: 'Py', tone: '#3776AB' },
{ label: 'SQL Server', mark: 'SQL', tone: '#CC2927' },
{ label: 'Power BI', mark: 'PBI', tone: '#F2C811' },
{ label: 'Databricks', mark: 'DB', tone: '#FF3621' },
{ label: 'Odoo', mark: 'Odo', tone: '#714B67' },
{ label: 'PostgreSQL', mark: 'PG', tone: '#336791' },
{ label: 'dbt', mark: 'dbt', tone: '#FF694B' },
{ label: 'Prefect', mark: 'Pf', tone: '#1E40AF' },
];
const row2 = [
{ label: 'Apps Script', mark: 'AS', tone: '#34A853' },
{ label: 'Git', mark: 'Git', tone: '#F05032' },
{ label: 'C#', mark: 'C#', tone: '#7F52FF' },
{ label: 'Golang', mark: 'Go', tone: '#00ADD8' },
{ label: 'SQLite', mark: 'Lite', tone: '#0F80CC' },
{ label: 'Azure Data Studio', mark: 'ADS', tone: '#0078D4' },
{ label: 'NotebookLM', mark: 'NB', tone: '#4285F4' },
{ label: 'Claude', mark: 'AI', tone: '#D97706' },
];
const d1 = [...row1, ...row1];
const d2 = [...row2, ...row2];
return (
{/* Fade left/right edges */}
);
}
function SkillsSection({ tweaks }) {
const primary = tweaks.primaryColor;
const skillGroups = [
{
title: 'Languages',
icon: 'M8 9l-4 3 4 3M16 9l4 3-4 3M14 4l-4 16',
items: [
{ label: 'Python', mark: 'Py', tone: '#3776AB' },
{ label: 'Apps Script', mark: 'AS', tone: '#34A853' },
{ label: 'C#', mark: 'C#', tone: '#7F52FF' },
{ label: 'Golang', mark: 'Go', tone: '#00ADD8' },
],
},
{
title: 'Databases',
icon: 'M4 6c0-1.7 3.6-3 8-3s8 1.3 8 3-3.6 3-8 3-8-1.3-8-3zm0 0v12c0 1.7 3.6 3 8 3s8-1.3 8-3V6M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3',
items: [
{ label: 'SQL Server', mark: 'SQL', tone: '#CC2927' },
{ label: 'PostgreSQL', mark: 'PG', tone: '#336791' },
{ label: 'SQLite', mark: 'Lite', tone: '#0F80CC' },
],
},
{
title: 'Tools',
icon: 'M6 3v6a3 3 0 003 3h6a3 3 0 013 3v6M6 9a3 3 0 100-6 3 3 0 000 6zM18 21a3 3 0 100-6 3 3 0 000 6z',
items: [
{ label: 'Git', mark: 'Git', tone: '#F05032' },
{ label: 'Azure Data Studio', mark: 'ADS', tone: '#0078D4' },
],
},
{
title: 'AI',
icon: 'M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8L12 3zM5 17l.9 2.1L8 20l-2.1.9L5 23l-.9-2.1L2 20l2.1-.9L5 17zM19 15l.7 1.6L21 17l-1.3.4L19 19l-.7-1.6L17 17l1.3-.4L19 15z',
items: [
{ label: 'NotebookLM', mark: 'LM', tone: '#4285F4' },
{ label: 'Claude', mark: 'Cl', tone: '#D97757' },
{ label: 'Codex', mark: 'Cx', tone: '#10A37F' },
{ label: 'Gemini', mark: 'Ge', tone: '#8E75FF' },
],
},
];
return (
Skills
{skillGroups.map(group => (
))}
);
}
function SkillGroupCard({ group, accent }) {
return (
{group.items.map(item => )}
);
}
function SkillItem({ item, accent }) {
const [hovered, setHovered] = React.useState(false);
return (
setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
display: 'flex', alignItems: 'center', gap: 10,
minHeight: 46, padding: '8px 10px', borderRadius: 14,
background: hovered ? `${accent}10` : 'rgba(var(--theme-bg-rgb),0.26)',
border: `1px solid ${hovered ? accent + '44' : 'rgba(var(--theme-text-rgb),0.08)'}`,
transition: 'background 0.22s ease, border-color 0.22s ease, transform 0.22s ease',
transform: hovered ? 'translateY(-1px)' : 'none',
}}>
2 ? 10 : 12,
lineHeight: 1, flexShrink: 0,
}}>{item.mark}
{item.label}
);
}
/* ─── Stack Card (sticky-stacking experience/education card) ─── */
function StackCard({ item, index, type, color }) {
const [hovered, setHovered] = React.useState(false);
return (
setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
position: 'relative',
padding: '36px 40px',
borderRadius: 24,
overflow: 'hidden',
background: hovered
? `linear-gradient(135deg, ${color}0E, rgba(var(--theme-surface-rgb),0.55))`
: `rgba(var(--theme-surface-rgb), ${0.18 + index * 0.06})`,
border: `1px solid ${hovered ? color + '2E' : 'rgba(var(--theme-text-rgb),0.07)'}`,
boxShadow: hovered
? `0 32px 64px rgba(0,0,0,0.28), 0 0 0 1px ${color}12`
: `0 ${8 + index * 4}px ${28 + index * 8}px rgba(0,0,0,${0.14 + index * 0.04})`,
backdropFilter: 'blur(14px)',
transition: 'all 0.4s cubic-bezier(0.25,0.46,0.45,0.94)',
}}>
{/* Content */}
{item.logo && (
)}
{item.role || item.degree}
{item.company || item.school}
{item.period}
{item.description}
{item.highlights && (
{item.highlights.map((h, idx) => (
{h}
))}
)}
);
}
/* Experience & Education Section */
function ExperienceSection({ tweaks }) {
const primary = tweaks.primaryColor;
const experiences = [
{
role: 'Data Analyst',
company: 'Tan Thanh Wood Joint Stocks Company',
logo: 'assets/tan-thanh-logo.png',
logoAlt: 'Tan Thanh Group logo',
period: '9/2025 - Present',
description: 'Gathered and analyzed business requirements from cross-functional departments to develop and optimize internal ERP applications. Managed the full software development lifecycle including requirements analysis, BPMN process design, stakeholder review, development, UAT, and production release.',
highlights: ['ERP Modules', 'BPMN Design', 'Process Digitization', 'UAT'],
},
{
role: 'Developer (Remote)',
company: 'Motico Company Limited',
logo: 'assets/motico-logo.png',
logoAlt: 'Motico logo',
period: '3/2026 - Present',
description: 'Received business requirements directly from the General Director to design and build CRM and task management workflows. Focused on customer pipeline tracking, follow-up activities, task assignment, progress status, and internal collaboration features for daily operations.',
highlights: ['CRM Workflow', 'Customer Pipeline', 'Task Management', 'Progress Tracking'],
},
{
role: 'Intern',
company: 'VNPT Can Tho',
logo: 'assets/vnpt-logo.png',
logoAlt: 'VNPT logo',
period: '5/2024 - 8/2024',
description: 'Supported healthcare management system development under mentor guidance, gaining hands-on exposure to professional software development practices.',
highlights: ['Healthcare System', 'Software Development'],
},
];
const education = [
{
degree: 'Software Engineering',
school: 'Can Tho University',
logo: 'assets/can-tho-university-logo.png',
logoAlt: 'Can Tho University logo',
period: '2019 - 2024',
description: 'Focused on software engineering, database systems, and data science. Graduated with a GPA of 7.76/10.',
},
];
return (
{/* Experience */}
Work Experience
{experiences.map((exp, i) => (
))}
{/* Education */}
Education
{education.map((edu, i) => (
))}
);
}
function TimelineCard({ item, type, color, isLast }) {
const [hovered, setHovered] = React.useState(false);
return (
setHovered(true)} onMouseLeave={() => setHovered(false)}>
{/* Dot */}
{/* Card */}
{item.logo && (
)}
{item.role || item.degree}
{item.company || item.school}
{item.period}
{item.description}
{item.highlights && (
{item.highlights.map((h, i) => (
{h}
))}
)}
);
}
/* Projects Section */
function ProjectsSection({ onSelectProject, tweaks }) {
const primary = tweaks.primaryColor;
const [filter, setFilter] = React.useState('All');
const [hoveredFilter, setHoveredFilter] = React.useState(null);
const categories = ['All', ...Array.from(new Set(PROJECTS.map(p => p.category)))];
const filtered = filter === 'All' ? PROJECTS : PROJECTS.filter(p => p.category === filter);
return (
My recent works
{categories.map(c => {
const active = filter === c;
const hovered = hoveredFilter === c;
return (
setFilter(c)}
onMouseEnter={() => setHoveredFilter(c)}
onMouseLeave={() => setHoveredFilter(null)}
style={{
padding: '10px 20px', borderRadius: 14,
border: `1px solid ${active || hovered ? primary + '66' : 'rgba(var(--theme-text-rgb),0.10)'}`,
background: active || hovered
? `linear-gradient(180deg, ${primary}16, rgba(var(--theme-surface-rgb),0.48))`
: 'rgba(var(--theme-surface-rgb),0.42)',
color: active || hovered ? primary : 'rgba(var(--theme-text-rgb),0.78)',
fontFamily: 'Poppins, sans-serif', fontWeight: 700,
fontSize: 14, cursor: 'pointer',
transition: 'background 0.24s ease, border-color 0.24s ease, color 0.24s ease, transform 0.24s ease',
transform: hovered ? 'translateY(-1px)' : 'none',
boxShadow: active || hovered ? '0 12px 28px rgba(0,0,0,0.22)' : 'none',
}}>{c}
)})}
{filtered.map((p, i) => (
))}
);
}
/* Contact Section */
function ContactSection({ tweaks }) {
const primary = tweaks.primaryColor;
const [form, setForm] = React.useState({ name: '', email: '', message: '' });
const [sent, setSent] = React.useState(false);
const [focused, setFocused] = React.useState(null);
const [submitHovered, setSubmitHovered] = React.useState(false);
const handleSubmit = (e) => {
e.preventDefault();
setSent(true);
setTimeout(() => setSent(false), 3000);
setForm({ name: '', email: '', message: '' });
};
const inputStyle = (field) => ({
width: '100%', padding: '16px 24px', borderRadius: 16,
background: 'rgba(var(--theme-surface-rgb),0.5)',
border: `2px solid ${focused === field ? primary : 'transparent'}`,
color: 'var(--theme-text)', fontFamily: 'Poppins, sans-serif', fontSize: 16,
outline: 'none', transition: 'all 0.3s', boxSizing: 'border-box',
});
return (
);
}
function ContactInfo({ icon, label, color }) {
return (
);
}