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 { import {
ArrowUpRight, ArrowUpRight,
Bot, Bot,
...@@ -72,6 +72,12 @@ const quickQuestions = [ ...@@ -72,6 +72,12 @@ const quickQuestions = [
'How can I contact Hung?', '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) { function answerQuestion(question: string) {
const normalized = question.toLowerCase() const normalized = question.toLowerCase()
...@@ -102,21 +108,42 @@ function answerQuestion(question: string) { ...@@ -102,21 +108,42 @@ function answerQuestion(question: string) {
function App() { function App() {
const [question, setQuestion] = useState(quickQuestions[0]) const [question, setQuestion] = useState(quickQuestions[0])
const [answer, setAnswer] = useState(answerQuestion(quickQuestions[0])) const [answer, setAnswer] = useState(answerQuestion(quickQuestions[0]))
const [answerPulse, setAnswerPulse] = useState(0)
const featuredSkills = useMemo( const featuredSkills = useMemo(
() => skillGroups.flatMap((group) => group.items).slice(0, 9), () => 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) { function handleAsk(nextQuestion = question) {
const trimmed = nextQuestion.trim() const trimmed = nextQuestion.trim()
if (!trimmed) return if (!trimmed) return
setQuestion(trimmed) setQuestion(trimmed)
setAnswer(answerQuestion(trimmed)) setAnswer(answerQuestion(trimmed))
setAnswerPulse((current) => current + 1)
} }
return ( return (
<main> <main>
<div className="ambient-grid" aria-hidden="true" />
<nav className="topbar" aria-label="Primary navigation"> <nav className="topbar" aria-label="Primary navigation">
<a className="brand" href="#hero">TQH</a> <a className="brand" href="#hero">TQH</a>
<div className="nav-links"> <div className="nav-links">
...@@ -128,9 +155,13 @@ function App() { ...@@ -128,9 +155,13 @@ function App() {
</nav> </nav>
<section className="hero" id="hero"> <section className="hero" id="hero">
<div className="hero-copy"> <div className="hero-copy reveal">
<p className="eyebrow">Mobile portfolio / React Native intern</p> <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="role">{profile.role}</p>
<p className="summary">{profile.summary}</p> <p className="summary">{profile.summary}</p>
...@@ -149,11 +180,22 @@ function App() { ...@@ -149,11 +180,22 @@ function App() {
<span>Firebase</span> <span>Firebase</span>
<span>Figma UI/UX</span> <span>Figma UI/UX</span>
</div> </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>
<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-frame">
<div className="phone-speaker" /> <div className="phone-speaker" />
<div className="screen-scan" aria-hidden="true" />
<img src="/cv-preview.png" alt="Tran Quang Hung CV preview" /> <img src="/cv-preview.png" alt="Tran Quang Hung CV preview" />
</div> </div>
<div className="floating-note"> <div className="floating-note">
...@@ -163,7 +205,7 @@ function App() { ...@@ -163,7 +205,7 @@ function App() {
</div> </div>
</section> </section>
<section className="section intro-grid" aria-label="Profile summary"> <section className="section intro-grid reveal" aria-label="Profile summary">
<div> <div>
<p className="section-kicker">Internship fit</p> <p className="section-kicker">Internship fit</p>
<h2>Building mobile interfaces with product-minded UI decisions.</h2> <h2>Building mobile interfaces with product-minded UI decisions.</h2>
...@@ -175,15 +217,16 @@ function App() { ...@@ -175,15 +217,16 @@ function App() {
</p> </p>
</section> </section>
<section className="section" id="projects"> <section className="section projects-section" id="projects">
<div className="section-heading"> <div className="section-heading reveal">
<p className="section-kicker">Selected work</p> <p className="section-kicker">Selected work</p>
<h2>Projects from my CV</h2> <h2>Projects from my CV</h2>
</div> </div>
<div className="project-grid"> <div className="project-grid">
{projects.map((project) => ( {projects.map((project, index) => (
<article className="project-card" key={project.name}> <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"> <div className="project-meta">
<span>{project.type}</span> <span>{project.type}</span>
<span>{project.time}</span> <span>{project.time}</span>
...@@ -208,7 +251,7 @@ function App() { ...@@ -208,7 +251,7 @@ function App() {
</section> </section>
<section className="section skills-section" id="skills"> <section className="section skills-section" id="skills">
<div className="section-heading"> <div className="section-heading reveal">
<p className="section-kicker">Capability map</p> <p className="section-kicker">Capability map</p>
<h2>Technical skills</h2> <h2>Technical skills</h2>
</div> </div>
...@@ -220,8 +263,8 @@ function App() { ...@@ -220,8 +263,8 @@ function App() {
</div> </div>
<div className="skill-groups"> <div className="skill-groups">
{skillGroups.map((group) => ( {skillGroups.map((group, index) => (
<div className="skill-group" key={group.title}> <div className="skill-group reveal" key={group.title} style={{ '--delay': `${index * 80}ms` } as CSSProperties}>
<h3>{group.title}</h3> <h3>{group.title}</h3>
<ul> <ul>
{group.items.map((item) => ( {group.items.map((item) => (
...@@ -234,7 +277,7 @@ function App() { ...@@ -234,7 +277,7 @@ function App() {
</section> </section>
<section className="section ai-panel" id="ai"> <section className="section ai-panel" id="ai">
<div className="ai-copy"> <div className="ai-copy reveal">
<p className="section-kicker">AI applied</p> <p className="section-kicker">AI applied</p>
<h2>Profile assistant</h2> <h2>Profile assistant</h2>
<p> <p>
...@@ -243,10 +286,11 @@ function App() { ...@@ -243,10 +286,11 @@ function App() {
</p> </p>
</div> </div>
<div className="assistant-box"> <div className="assistant-box reveal">
<div className="assistant-header"> <div className="assistant-header">
<Bot size={22} /> <Bot size={22} />
<span>Ask about Hung</span> <span>Ask about Hung</span>
<i aria-hidden="true" />
</div> </div>
<div className="quick-questions"> <div className="quick-questions">
...@@ -272,14 +316,14 @@ function App() { ...@@ -272,14 +316,14 @@ function App() {
</button> </button>
</div> </div>
<div className="answer-box"> <div className="answer-box" key={answerPulse}>
<span>Answer</span> <span>Answer</span>
<p>{answer}</p> <p>{answer}</p>
</div> </div>
</div> </div>
</section> </section>
<section className="section contact-section" id="contact"> <section className="section contact-section reveal" id="contact">
<div> <div>
<p className="section-kicker">Contact</p> <p className="section-kicker">Contact</p>
<h2>Ready for mobile app internship work.</h2> <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