Commit d31f77fc authored by HungTQ's avatar HungTQ

style(ui): enhance portfolio motion design

parent d288b585
:root {
--ink: #111827;
--muted: #5b6678;
--paper: #f5f0e8;
--surface: #fffaf1;
--line: #ded2c1;
--accent: #105c6d;
--accent-strong: #0d4855;
--signal: #d64d2a;
--night: #172025;
--shadow: 0 24px 70px rgba(28, 38, 43, 0.16);
--ink: #f6fbff;
--muted: #a7b7c8;
--dim: #6f8194;
--paper: #061018;
--surface: rgba(12, 26, 38, 0.78);
--surface-strong: rgba(16, 35, 49, 0.96);
--line: rgba(139, 213, 255, 0.18);
--line-strong: rgba(139, 213, 255, 0.38);
--accent: #42d9ff;
--accent-strong: #9df2ff;
--signal: #ff6b45;
--signal-soft: #ffb199;
--mint: #80ffcc;
--shadow: 0 34px 110px rgba(0, 0, 0, 0.48);
}
* {
......@@ -22,12 +26,44 @@ html {
body {
margin: 0;
background:
linear-gradient(90deg, rgba(17, 24, 39, 0.05) 1px, transparent 1px),
linear-gradient(rgba(17, 24, 39, 0.04) 1px, transparent 1px),
linear-gradient(135deg, rgba(66, 217, 255, 0.10), transparent 32%),
linear-gradient(225deg, rgba(255, 107, 69, 0.10), transparent 34%),
radial-gradient(circle at 50% 0%, rgba(128, 255, 204, 0.07), transparent 34%),
var(--paper);
background-size: 42px 42px;
color: var(--ink);
font-family: "Segoe UI", Candara, "Trebuchet MS", sans-serif;
overflow-x: hidden;
}
body::before {
animation: drift 22s linear infinite;
background-image:
linear-gradient(rgba(139, 213, 255, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(139, 213, 255, 0.05) 1px, transparent 1px);
background-size: 42px 42px;
content: "";
inset: 0;
pointer-events: none;
position: fixed;
z-index: -2;
}
body::after {
background:
repeating-linear-gradient(
90deg,
rgba(255, 255, 255, 0.025) 0,
rgba(255, 255, 255, 0.025) 1px,
transparent 1px,
transparent 4px
);
content: "";
inset: 0;
mix-blend-mode: overlay;
opacity: 0.55;
pointer-events: none;
position: fixed;
z-index: -1;
}
a {
......@@ -41,17 +77,28 @@ input {
main {
min-height: 100vh;
position: relative;
}
.ambient-grid {
background:
linear-gradient(115deg, transparent 0 38%, rgba(66, 217, 255, 0.10) 38% 39%, transparent 39% 100%),
linear-gradient(65deg, transparent 0 60%, rgba(255, 107, 69, 0.10) 60% 61%, transparent 61% 100%);
inset: 0;
pointer-events: none;
position: fixed;
z-index: -1;
}
.topbar {
align-items: center;
backdrop-filter: blur(18px);
background: rgba(245, 240, 232, 0.84);
border-bottom: 1px solid rgba(222, 210, 193, 0.8);
backdrop-filter: blur(22px);
background: rgba(6, 16, 24, 0.74);
border-bottom: 1px solid var(--line);
display: flex;
justify-content: space-between;
left: 0;
padding: 18px clamp(18px, 5vw, 64px);
padding: 16px clamp(18px, 5vw, 72px);
position: sticky;
right: 0;
top: 0;
......@@ -60,54 +107,73 @@ main {
.brand {
align-items: center;
background: var(--night);
background: linear-gradient(135deg, var(--accent), var(--mint));
border-radius: 8px;
color: #fffaf1;
color: #061018;
display: inline-flex;
font-weight: 900;
height: 40px;
font-weight: 950;
height: 42px;
justify-content: center;
letter-spacing: 0.08em;
text-decoration: none;
width: 48px;
width: 52px;
}
.nav-links {
display: flex;
gap: 22px;
gap: 8px;
}
.nav-links a {
border: 1px solid transparent;
border-radius: 8px;
color: var(--muted);
font-size: 14px;
font-weight: 800;
font-size: 13px;
font-weight: 850;
padding: 9px 12px;
text-decoration: none;
transition: border-color 180ms ease, color 180ms ease, background 180ms ease;
}
.nav-links a:hover {
background: rgba(139, 213, 255, 0.08);
border-color: var(--line);
color: var(--ink);
}
.hero {
display: grid;
gap: clamp(28px, 5vw, 72px);
grid-template-columns: minmax(0, 1.04fr) minmax(320px, 0.8fr);
min-height: calc(100vh - 77px);
padding: clamp(48px, 7vw, 92px) clamp(18px, 5vw, 64px) 56px;
gap: clamp(30px, 5vw, 86px);
grid-template-columns: minmax(0, 1.02fr) minmax(340px, 0.82fr);
min-height: calc(100vh - 75px);
overflow: hidden;
padding: clamp(58px, 8vw, 110px) clamp(18px, 5vw, 72px) 68px;
position: relative;
}
.hero::before {
animation: sweep 8s ease-in-out infinite;
background: linear-gradient(90deg, transparent, rgba(66, 217, 255, 0.16), transparent);
content: "";
height: 1px;
left: -20%;
position: absolute;
top: 17%;
width: 140%;
}
.hero-copy {
align-self: center;
max-width: 760px;
max-width: 790px;
}
.eyebrow,
.section-kicker {
color: var(--accent);
font-size: 13px;
font-weight: 900;
letter-spacing: 0.12em;
margin: 0 0 14px;
font-weight: 950;
letter-spacing: 0.14em;
margin: 0 0 16px;
text-transform: uppercase;
}
......@@ -120,16 +186,30 @@ p {
h1 {
font-family: Georgia, "Times New Roman", serif;
font-size: clamp(56px, 10vw, 124px);
line-height: 0.88;
margin-bottom: 20px;
max-width: 900px;
font-size: clamp(58px, 10vw, 132px);
line-height: 0.82;
margin-bottom: 24px;
max-width: 980px;
}
h1 span {
animation: titleRise 780ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
display: block;
text-shadow: 0 0 34px rgba(66, 217, 255, 0.18);
}
h1 span:nth-child(2) {
animation-delay: 80ms;
}
h1 span:nth-child(3) {
animation-delay: 160ms;
}
h2 {
font-family: Georgia, "Times New Roman", serif;
font-size: clamp(34px, 5vw, 64px);
line-height: 1;
font-size: clamp(34px, 5vw, 68px);
line-height: 0.98;
margin-bottom: 0;
}
......@@ -140,24 +220,24 @@ h3 {
}
.role {
color: var(--signal);
font-size: clamp(22px, 3vw, 32px);
font-weight: 900;
color: var(--signal-soft);
font-size: clamp(22px, 3vw, 34px);
font-weight: 950;
margin-bottom: 18px;
}
.summary {
color: var(--muted);
font-size: clamp(17px, 2vw, 22px);
line-height: 1.55;
max-width: 720px;
line-height: 1.58;
max-width: 760px;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin: 32px 0;
margin: 34px 0;
}
.primary-action,
......@@ -165,29 +245,36 @@ h3 {
align-items: center;
border-radius: 8px;
display: inline-flex;
font-weight: 900;
font-weight: 950;
gap: 8px;
min-height: 48px;
min-height: 50px;
padding: 0 18px;
text-decoration: none;
transform: translateZ(0);
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
}
.primary-action {
background: var(--accent);
color: #fffaf1;
background: linear-gradient(135deg, var(--accent), var(--mint));
box-shadow: 0 18px 46px rgba(66, 217, 255, 0.22);
color: #061018;
}
.primary-action:hover {
background: var(--accent-strong);
.primary-action:hover,
.secondary-action:hover {
transform: translateY(-3px);
}
.secondary-action {
background: rgba(255, 250, 241, 0.8);
background: rgba(12, 26, 38, 0.78);
border: 1px solid var(--line);
color: var(--ink);
}
.signal-row {
.signal-row,
.skill-cloud,
.stack-list,
.project-links {
display: flex;
flex-wrap: wrap;
gap: 10px;
......@@ -196,38 +283,85 @@ h3 {
.signal-row span,
.skill-cloud span,
.stack-list span {
background: #fffaf1;
background: rgba(12, 26, 38, 0.82);
border: 1px solid var(--line);
border-radius: 8px;
color: var(--accent-strong);
display: inline-flex;
font-size: 13px;
font-weight: 900;
font-weight: 950;
padding: 8px 10px;
}
.hero-metrics {
display: grid;
gap: 10px;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: 28px;
max-width: 650px;
}
.hero-metrics div {
background: rgba(12, 26, 38, 0.66);
border: 1px solid var(--line);
border-radius: 8px;
padding: 14px;
}
.hero-metrics strong,
.hero-metrics span {
display: block;
}
.hero-metrics strong {
color: var(--ink);
font-size: 22px;
margin-bottom: 5px;
}
.hero-metrics span {
color: var(--dim);
font-size: 12px;
font-weight: 850;
text-transform: uppercase;
}
.hero-visual {
align-items: center;
display: flex;
justify-content: center;
min-height: 520px;
min-height: 560px;
position: relative;
}
.device-halo {
animation: pulseGlow 4.6s ease-in-out infinite;
background:
conic-gradient(from 120deg, rgba(66, 217, 255, 0), rgba(66, 217, 255, 0.55), rgba(255, 107, 69, 0.45), rgba(66, 217, 255, 0));
border-radius: 40px;
filter: blur(30px);
height: 78%;
opacity: 0.62;
position: absolute;
transform: rotate(8deg);
width: min(88%, 440px);
}
.phone-frame {
background: #1b252b;
border: 12px solid #1b252b;
border-radius: 30px;
box-shadow: var(--shadow);
max-width: 360px;
animation: deviceFloat 5.5s ease-in-out infinite;
background: #0a151e;
border: 12px solid #111f2a;
border-radius: 32px;
box-shadow: var(--shadow), inset 0 0 0 1px rgba(255, 255, 255, 0.06);
max-width: 365px;
overflow: hidden;
position: relative;
transform: rotate(2deg);
width: min(100%, 360px);
width: min(100%, 365px);
}
.phone-speaker {
background: #303c42;
background: #2c3c47;
border-radius: 999px;
height: 6px;
left: 50%;
......@@ -235,58 +369,78 @@ h3 {
top: 12px;
transform: translateX(-50%);
width: 76px;
z-index: 3;
}
.screen-scan {
animation: scan 3.6s ease-in-out infinite;
background: linear-gradient(180deg, transparent, rgba(66, 217, 255, 0.30), transparent);
height: 24%;
left: 0;
pointer-events: none;
position: absolute;
right: 0;
top: -30%;
z-index: 2;
}
.phone-frame img {
display: block;
filter: saturate(0.92) contrast(1.04);
filter: saturate(1.02) contrast(1.07);
width: 100%;
}
.floating-note {
align-items: center;
background: var(--signal);
animation: noteBob 4.2s ease-in-out infinite;
background: linear-gradient(135deg, var(--signal), #ff9a62);
border-radius: 8px;
bottom: 46px;
box-shadow: var(--shadow);
color: #fffaf1;
bottom: 54px;
box-shadow: 0 22px 50px rgba(255, 107, 69, 0.26);
color: #fff;
display: flex;
font-weight: 900;
font-weight: 950;
gap: 10px;
padding: 14px 16px;
position: absolute;
right: 4%;
right: 2%;
}
.section {
padding: clamp(52px, 7vw, 92px) clamp(18px, 5vw, 64px);
padding: clamp(56px, 7vw, 98px) clamp(18px, 5vw, 72px);
position: relative;
}
.intro-grid {
align-items: end;
background: var(--night);
color: #fffaf1;
background:
linear-gradient(135deg, rgba(66, 217, 255, 0.13), transparent 38%),
#09151e;
border-block: 1px solid var(--line);
display: grid;
gap: 32px;
grid-template-columns: minmax(0, 0.95fr) minmax(280px, 0.8fr);
}
.intro-grid .section-kicker {
color: #f2b39f;
color: var(--mint);
}
.intro-grid p:last-child {
color: #c8d2d3;
color: var(--muted);
font-size: 19px;
line-height: 1.6;
line-height: 1.65;
}
.section-heading {
align-items: end;
display: flex;
justify-content: space-between;
margin-bottom: 28px;
margin-bottom: 32px;
}
.projects-section {
background: linear-gradient(180deg, rgba(11, 28, 40, 0.82), rgba(6, 16, 24, 0.94));
}
.project-grid {
......@@ -299,59 +453,98 @@ h3 {
.assistant-box,
.skill-group,
.contact-list {
background: rgba(255, 250, 241, 0.9);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: 0 12px 34px rgba(28, 38, 43, 0.08);
box-shadow: 0 18px 55px rgba(0, 0, 0, 0.22);
}
.project-card {
padding: clamp(22px, 3vw, 32px);
min-height: 330px;
overflow: hidden;
padding: clamp(22px, 3vw, 34px);
position: relative;
transition: border-color 220ms ease, transform 220ms ease, box-shadow 220ms ease;
}
.project-card::before {
background: linear-gradient(135deg, rgba(66, 217, 255, 0.18), transparent 45%);
content: "";
inset: 0;
opacity: 0;
position: absolute;
transition: opacity 220ms ease;
}
.project-card:hover {
border-color: var(--line-strong);
box-shadow: 0 26px 80px rgba(66, 217, 255, 0.13);
transform: translateY(-6px);
}
.project-card:hover::before {
opacity: 1;
}
.project-card > * {
position: relative;
z-index: 1;
}
.project-index {
color: rgba(157, 242, 255, 0.16);
font-family: Georgia, "Times New Roman", serif;
font-size: 106px;
font-weight: 900;
line-height: 0.8;
position: absolute;
right: 24px;
top: 22px;
}
.project-meta {
color: var(--muted);
color: var(--dim);
display: flex;
flex-wrap: wrap;
font-size: 13px;
font-weight: 900;
font-weight: 950;
gap: 10px;
justify-content: space-between;
margin-bottom: 18px;
margin-bottom: 20px;
text-transform: uppercase;
}
.project-card p {
color: var(--muted);
line-height: 1.58;
line-height: 1.62;
}
.stack-list,
.project-links {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 18px;
margin-top: 20px;
}
.project-links a {
align-items: center;
color: var(--signal);
color: var(--signal-soft);
display: inline-flex;
font-weight: 900;
font-weight: 950;
gap: 4px;
text-decoration: none;
}
.project-links a:hover {
color: #fff;
}
.skills-section {
background: #f0e3d3;
background:
linear-gradient(90deg, rgba(128, 255, 204, 0.08), transparent 40%),
#07111a;
}
.skill-cloud {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 28px;
margin-bottom: 30px;
}
.skill-groups {
......@@ -361,16 +554,23 @@ h3 {
}
.skill-group {
padding: 20px;
padding: 22px;
transition: transform 200ms ease, border-color 200ms ease;
}
.skill-group:hover {
border-color: var(--line-strong);
transform: translateY(-4px);
}
.skill-group h3 {
color: var(--accent-strong);
font-size: 18px;
}
.skill-group ul {
color: var(--muted);
line-height: 1.8;
line-height: 1.85;
list-style: none;
margin: 0;
padding: 0;
......@@ -378,20 +578,36 @@ h3 {
.ai-panel {
align-items: start;
background:
linear-gradient(135deg, rgba(255, 107, 69, 0.10), transparent 35%),
#061018;
display: grid;
gap: clamp(24px, 5vw, 72px);
grid-template-columns: minmax(0, 0.8fr) minmax(320px, 1fr);
grid-template-columns: minmax(0, 0.78fr) minmax(330px, 1fr);
}
.ai-copy p:last-child {
color: var(--muted);
font-size: 18px;
line-height: 1.6;
line-height: 1.64;
margin-top: 18px;
}
.assistant-box {
overflow: hidden;
padding: clamp(20px, 4vw, 32px);
position: relative;
}
.assistant-box::before {
animation: assistantRail 5s linear infinite;
background: linear-gradient(90deg, transparent, rgba(128, 255, 204, 0.34), transparent);
content: "";
height: 1px;
left: -40%;
position: absolute;
top: 0;
width: 80%;
}
.assistant-header {
......@@ -399,9 +615,18 @@ h3 {
color: var(--accent-strong);
display: flex;
font-size: 18px;
font-weight: 900;
font-weight: 950;
gap: 10px;
margin-bottom: 18px;
margin-bottom: 20px;
}
.assistant-header i {
animation: blink 1.3s steps(2, end) infinite;
background: var(--mint);
border-radius: 999px;
height: 8px;
margin-left: auto;
width: 8px;
}
.quick-questions {
......@@ -412,21 +637,27 @@ h3 {
}
.quick-questions button {
background: #f0e3d3;
background: rgba(66, 217, 255, 0.08);
border: 1px solid var(--line);
border-radius: 8px;
color: var(--ink);
cursor: pointer;
font-size: 13px;
font-weight: 800;
font-weight: 850;
padding: 8px 10px;
transition: background 180ms ease, transform 180ms ease;
}
.quick-questions button:hover {
background: rgba(66, 217, 255, 0.16);
transform: translateY(-2px);
}
.input-label {
color: var(--muted);
color: var(--dim);
display: block;
font-size: 13px;
font-weight: 900;
font-weight: 950;
margin-bottom: 8px;
text-transform: uppercase;
}
......@@ -434,72 +665,79 @@ h3 {
.ask-row {
display: grid;
gap: 8px;
grid-template-columns: minmax(0, 1fr) 48px;
grid-template-columns: minmax(0, 1fr) 50px;
}
.ask-row input {
background: #fffdf8;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--line);
border-radius: 8px;
color: var(--ink);
min-height: 48px;
min-height: 50px;
padding: 0 12px;
width: 100%;
}
.ask-row input:focus {
border-color: var(--accent);
outline: none;
}
.ask-row button {
align-items: center;
background: var(--accent);
background: linear-gradient(135deg, var(--accent), var(--mint));
border: 0;
border-radius: 8px;
color: #fffaf1;
color: #061018;
cursor: pointer;
display: inline-flex;
justify-content: center;
}
.answer-box {
background: var(--night);
animation: answerIn 260ms ease both;
background: #02080d;
border: 1px solid rgba(128, 255, 204, 0.20);
border-radius: 8px;
color: #fffaf1;
color: #fff;
margin-top: 16px;
padding: 18px;
}
.answer-box span {
color: #f2b39f;
color: var(--mint);
display: block;
font-size: 13px;
font-weight: 900;
font-weight: 950;
margin-bottom: 8px;
text-transform: uppercase;
}
.answer-box p {
color: #dce4e5;
line-height: 1.55;
color: #dce7ef;
line-height: 1.58;
margin: 0;
}
.contact-section {
align-items: start;
background: var(--night);
color: #fffaf1;
background: #02080d;
border-top: 1px solid var(--line);
display: grid;
gap: 32px;
grid-template-columns: minmax(0, 0.95fr) minmax(320px, 0.8fr);
}
.contact-section .section-kicker {
color: #f2b39f;
color: var(--signal-soft);
}
.contact-list {
background: rgba(255, 250, 241, 0.08);
border-color: rgba(255, 250, 241, 0.15);
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.12);
display: grid;
gap: 10px;
padding: 20px;
gap: 12px;
padding: 22px;
}
.contact-list a,
......@@ -512,20 +750,145 @@ footer span {
.contact-list a,
.contact-list span {
color: #dce4e5;
color: #dce7ef;
text-decoration: none;
}
.contact-list a:hover {
color: var(--accent-strong);
}
footer {
align-items: center;
background: #0d1317;
color: #bac5c7;
background: #02080d;
border-top: 1px solid rgba(255, 255, 255, 0.08);
color: var(--dim);
display: flex;
flex-wrap: wrap;
font-size: 14px;
gap: 12px;
justify-content: space-between;
padding: 18px clamp(18px, 5vw, 64px);
padding: 18px clamp(18px, 5vw, 72px);
}
.reveal {
animation: revealIn 760ms cubic-bezier(0.2, 0.8, 0.2, 1) var(--delay, 0ms) both;
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
}
@keyframes titleRise {
from {
opacity: 0;
transform: translateY(32px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes drift {
from {
background-position: 0 0;
}
to {
background-position: 42px 42px;
}
}
@keyframes sweep {
0%,
100% {
opacity: 0.05;
transform: translateX(-12%);
}
50% {
opacity: 0.7;
transform: translateX(12%);
}
}
@keyframes deviceFloat {
0%,
100% {
transform: rotate(2deg) translateY(0);
}
50% {
transform: rotate(-1deg) translateY(-14px);
}
}
@keyframes pulseGlow {
0%,
100% {
opacity: 0.44;
transform: rotate(8deg) scale(0.96);
}
50% {
opacity: 0.76;
transform: rotate(14deg) scale(1.04);
}
}
@keyframes scan {
0% {
top: -30%;
}
55%,
100% {
top: 112%;
}
}
@keyframes noteBob {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-9px);
}
}
@keyframes assistantRail {
from {
transform: translateX(0);
}
to {
transform: translateX(180%);
}
}
@keyframes blink {
50% {
opacity: 0.25;
}
}
@keyframes answerIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes revealIn {
from {
opacity: 0;
transform: translateY(26px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 900px) {
......@@ -548,6 +911,7 @@ footer {
min-height: auto;
}
.hero-metrics,
.project-grid,
.skill-groups {
grid-template-columns: 1fr;
......@@ -560,6 +924,23 @@ footer {
.floating-note {
bottom: 18px;
max-width: calc(100% - 20px);
right: 0;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation: none !important;
scroll-behavior: auto !important;
transition: none !important;
}
.reveal {
animation: none !important;
opacity: 1;
transform: none;
}
}
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