Commit d31f77fc authored by HungTQ's avatar HungTQ

style(ui): enhance portfolio motion design

parent d288b585
This diff is collapsed.
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState, type CSSProperties } from 'react'
import {
ArrowUpRight,
Bot,
......@@ -72,6 +72,12 @@ const quickQuestions = [
'How can I contact Hung?',
]
const metrics = [
{ label: 'Mobile projects', value: '02' },
{ label: 'Core stack', value: 'RN + Flutter' },
{ label: 'Graduation', value: '2027' },
]
function answerQuestion(question: string) {
const normalized = question.toLowerCase()
......@@ -102,21 +108,42 @@ function answerQuestion(question: string) {
function App() {
const [question, setQuestion] = useState(quickQuestions[0])
const [answer, setAnswer] = useState(answerQuestion(quickQuestions[0]))
const [answerPulse, setAnswerPulse] = useState(0)
const featuredSkills = useMemo(
() => skillGroups.flatMap((group) => group.items).slice(0, 9),
[],
)
useEffect(() => {
const revealItems = document.querySelectorAll<HTMLElement>('.reveal')
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible')
observer.unobserve(entry.target)
}
})
},
{ threshold: 0.18 },
)
revealItems.forEach((item) => observer.observe(item))
return () => observer.disconnect()
}, [])
function handleAsk(nextQuestion = question) {
const trimmed = nextQuestion.trim()
if (!trimmed) return
setQuestion(trimmed)
setAnswer(answerQuestion(trimmed))
setAnswerPulse((current) => current + 1)
}
return (
<main>
<div className="ambient-grid" aria-hidden="true" />
<nav className="topbar" aria-label="Primary navigation">
<a className="brand" href="#hero">TQH</a>
<div className="nav-links">
......@@ -128,9 +155,13 @@ function App() {
</nav>
<section className="hero" id="hero">
<div className="hero-copy">
<div className="hero-copy reveal">
<p className="eyebrow">Mobile portfolio / React Native intern</p>
<h1>{profile.name}</h1>
<h1>
<span>Tran</span>
<span>Quang</span>
<span>Hung</span>
</h1>
<p className="role">{profile.role}</p>
<p className="summary">{profile.summary}</p>
......@@ -149,11 +180,22 @@ function App() {
<span>Firebase</span>
<span>Figma UI/UX</span>
</div>
<div className="hero-metrics" aria-label="Profile metrics">
{metrics.map((metric) => (
<div key={metric.label}>
<strong>{metric.value}</strong>
<span>{metric.label}</span>
</div>
))}
</div>
</div>
<div className="hero-visual" aria-label="CV preview">
<div className="hero-visual reveal" aria-label="CV preview">
<div className="device-halo" aria-hidden="true" />
<div className="phone-frame">
<div className="phone-speaker" />
<div className="screen-scan" aria-hidden="true" />
<img src="/cv-preview.png" alt="Tran Quang Hung CV preview" />
</div>
<div className="floating-note">
......@@ -163,7 +205,7 @@ function App() {
</div>
</section>
<section className="section intro-grid" aria-label="Profile summary">
<section className="section intro-grid reveal" aria-label="Profile summary">
<div>
<p className="section-kicker">Internship fit</p>
<h2>Building mobile interfaces with product-minded UI decisions.</h2>
......@@ -175,15 +217,16 @@ function App() {
</p>
</section>
<section className="section" id="projects">
<div className="section-heading">
<section className="section projects-section" id="projects">
<div className="section-heading reveal">
<p className="section-kicker">Selected work</p>
<h2>Projects from my CV</h2>
</div>
<div className="project-grid">
{projects.map((project) => (
<article className="project-card" key={project.name}>
{projects.map((project, index) => (
<article className="project-card reveal" key={project.name} style={{ '--delay': `${index * 110}ms` } as CSSProperties}>
<div className="project-index">0{index + 1}</div>
<div className="project-meta">
<span>{project.type}</span>
<span>{project.time}</span>
......@@ -208,7 +251,7 @@ function App() {
</section>
<section className="section skills-section" id="skills">
<div className="section-heading">
<div className="section-heading reveal">
<p className="section-kicker">Capability map</p>
<h2>Technical skills</h2>
</div>
......@@ -220,8 +263,8 @@ function App() {
</div>
<div className="skill-groups">
{skillGroups.map((group) => (
<div className="skill-group" key={group.title}>
{skillGroups.map((group, index) => (
<div className="skill-group reveal" key={group.title} style={{ '--delay': `${index * 80}ms` } as CSSProperties}>
<h3>{group.title}</h3>
<ul>
{group.items.map((item) => (
......@@ -234,7 +277,7 @@ function App() {
</section>
<section className="section ai-panel" id="ai">
<div className="ai-copy">
<div className="ai-copy reveal">
<p className="section-kicker">AI applied</p>
<h2>Profile assistant</h2>
<p>
......@@ -243,10 +286,11 @@ function App() {
</p>
</div>
<div className="assistant-box">
<div className="assistant-box reveal">
<div className="assistant-header">
<Bot size={22} />
<span>Ask about Hung</span>
<i aria-hidden="true" />
</div>
<div className="quick-questions">
......@@ -272,14 +316,14 @@ function App() {
</button>
</div>
<div className="answer-box">
<div className="answer-box" key={answerPulse}>
<span>Answer</span>
<p>{answer}</p>
</div>
</div>
</section>
<section className="section contact-section" id="contact">
<section className="section contact-section reveal" id="contact">
<div>
<p className="section-kicker">Contact</p>
<h2>Ready for mobile app internship work.</h2>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment