feat(ui): simplify skills display and fix contact heading and static labels

This commit is contained in:
ssbg04 committed 2026-09-12 13:38:54 +08:00
1 parent 5f2593db2a
commit b2a5ef1a31
4 files changed
+192 -123

No files matched your search

+7
View File
@@ -34,3 +34,10 @@ Verified digital credentials from Cisco and industry partners. Each badge repres
- certifications section -> all categories use simple word and short for title category
- only fetch spotify widget when modal is open, fix recent play its pausing
- in tech stack technology add icon fontawesome and update the sanity io -->
<!-- # TECHNOLOGIES & TOOLS
- make the per categories simple and easy to read, remove some uneccessary components, remove the per skills has its box
# CONTACT SECTION
- fix the "Let's create something amazing." is under lapping to paragraph element
- in ultra fast mode the form still has label animation, fix the label to be static and dont move or animate, always show the label above the input field -->
+154 -73
View File
@@ -56,6 +56,21 @@ export default function ContactForm({
const [formData, setFormData] = useState({ name: '', email: '', message: '' });
const [status, setStatus] = useState<'idle' | 'sending' | 'success' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState('');
const [isLowTier, setIsLowTier] = useState(() => {
if (typeof document !== 'undefined') {
return document.documentElement.dataset.tier === 'low' || localStorage.getItem('liteMode') === 'true';
}
return false;
});
React.useEffect(() => {
const checkTier = () => {
setIsLowTier(document.documentElement.dataset.tier === 'low' || localStorage.getItem('liteMode') === 'true');
};
checkTier();
window.addEventListener('tier-change', checkTier);
return () => window.removeEventListener('tier-change', checkTier);
}, []);
const linksToRender = socialLinks.length > 0
? socialLinks.filter(l => l.url && l.url !== '#' && !l.url.includes('/N/A'))
@@ -112,7 +127,7 @@ export default function ContactForm({
{/* Left Side: Info */}
<div className="lg:col-span-2 flex flex-col justify-center h-full">
<ScrollReveal variant="fade-right">
<h2 className="text-4xl md:text-5xl font-black font-heading mb-6 text-glow bg-clip-text text-transparent bg-gradient-to-r from-foreground-custom to-primary-custom/80">
<h2 className="text-3xl sm:text-4xl md:text-5xl font-black font-heading mb-5 leading-tight tracking-tight text-foreground-custom block">
{contactHeading}
</h2>
<p className="text-muted-foreground-custom text-base md:text-lg mb-8 leading-relaxed">
@@ -148,9 +163,9 @@ export default function ContactForm({
{status === 'success' ? (
<motion.div
key="success"
initial={{ opacity: 0, scale: 0.95 }}
initial={isLowTier ? false : { opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.05 }}
exit={isLowTier ? undefined : { opacity: 0, scale: 1.05 }}
className="text-center py-12 flex flex-col items-center justify-center min-h-[400px]"
>
<div className="w-20 h-20 rounded-full bg-emerald-500/10 text-emerald-500 flex items-center justify-center mb-6 shadow-[0_0_30px_rgba(16,185,129,0.2)]">
@@ -166,7 +181,7 @@ export default function ContactForm({
</p>
<button
onClick={() => setStatus('idle')}
className="px-8 py-3.5 rounded-full bg-foreground-custom/5 hover:bg-foreground-custom/10 text-foreground-custom text-sm font-bold shadow-sm hover:scale-[1.02] active:scale-[0.98] transition-all"
className="px-8 py-3.5 rounded-full bg-foreground-custom/5 hover:bg-foreground-custom/10 text-foreground-custom text-sm font-bold shadow-sm hover:scale-[1.02] active:scale-[0.98] transition-all cursor-pointer"
>
Send Another
</button>
@@ -174,88 +189,153 @@ export default function ContactForm({
) : (
<motion.form
key="form"
initial={{ opacity: 0 }}
initial={isLowTier ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
exit={isLowTier ? undefined : { opacity: 0 }}
onSubmit={handleSubmit}
className="flex flex-col gap-8"
className="flex flex-col gap-6"
>
{status === 'error' && (
<div className="p-4 rounded-2xl bg-rose-500/10 border border-rose-500/20 text-rose-500 text-sm font-medium">
<div className="p-4 rounded-xl bg-rose-500/10 border border-rose-500/20 text-rose-500 text-sm font-medium">
{errorMessage}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Floating Name */}
<div className="relative z-0 w-full group">
<input
type="text"
name="name"
id="name"
className="block py-3 px-0 w-full text-base text-foreground-custom bg-transparent border-0 border-b-2 border-border-hover/30 appearance-none focus:outline-none focus:ring-0 focus:border-primary-custom peer transition-colors"
placeholder=" "
required
value={formData.name}
onChange={handleChange}
disabled={status === 'sending'}
/>
<label
htmlFor="name"
className="peer-focus:font-medium absolute text-sm text-muted-foreground-custom duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:left-0 peer-focus:text-primary-custom peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
>
Full Name
</label>
</div>
{/* Low-tier / Ultra-fast mode: Always Static Labels Above Inputs (No animation) */}
{isLowTier ? (
<div className="flex flex-col gap-5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{/* Static Full Name */}
<div className="flex flex-col gap-1.5 w-full">
<label htmlFor="name" className="text-xs font-semibold text-foreground-custom">
Full Name
</label>
<input
type="text"
name="name"
id="name"
className="block py-2.5 px-3 w-full text-sm text-foreground-custom bg-background-custom border border-border-custom rounded-md focus:outline-none focus:border-primary-custom"
placeholder="Your full name"
required
value={formData.name}
onChange={handleChange}
disabled={status === 'sending'}
/>
</div>
{/* Floating Email */}
<div className="relative z-0 w-full group">
<input
type="email"
name="email"
id="email"
className="block py-3 px-0 w-full text-base text-foreground-custom bg-transparent border-0 border-b-2 border-border-hover/30 appearance-none focus:outline-none focus:ring-0 focus:border-primary-custom peer transition-colors"
placeholder=" "
required
value={formData.email}
onChange={handleChange}
disabled={status === 'sending'}
/>
<label
htmlFor="email"
className="peer-focus:font-medium absolute text-sm text-muted-foreground-custom duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:left-0 peer-focus:text-primary-custom peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
>
Email Address
</label>
</div>
</div>
{/* Static Email */}
<div className="flex flex-col gap-1.5 w-full">
<label htmlFor="email" className="text-xs font-semibold text-foreground-custom">
Email Address
</label>
<input
type="email"
name="email"
id="email"
className="block py-2.5 px-3 w-full text-sm text-foreground-custom bg-background-custom border border-border-custom rounded-md focus:outline-none focus:border-primary-custom"
placeholder="you@example.com"
required
value={formData.email}
onChange={handleChange}
disabled={status === 'sending'}
/>
</div>
</div>
{/* Floating Message */}
<div className="relative z-0 w-full group mt-2">
<textarea
name="message"
id="message"
rows={4}
className="block py-3 px-0 w-full text-base text-foreground-custom bg-transparent border-0 border-b-2 border-border-hover/30 appearance-none focus:outline-none focus:ring-0 focus:border-primary-custom peer transition-colors resize-none"
placeholder=" "
required
value={formData.message}
onChange={handleChange}
disabled={status === 'sending'}
/>
<label
htmlFor="message"
className="peer-focus:font-medium absolute text-sm text-muted-foreground-custom duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:left-0 peer-focus:text-primary-custom peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
>
How can I help you?
</label>
</div>
{/* Static Message */}
<div className="flex flex-col gap-1.5 w-full">
<label htmlFor="message" className="text-xs font-semibold text-foreground-custom">
How can I help you?
</label>
<textarea
name="message"
id="message"
rows={4}
className="block py-2.5 px-3 w-full text-sm text-foreground-custom bg-background-custom border border-border-custom rounded-md focus:outline-none focus:border-primary-custom resize-none"
placeholder="Your message or project inquiry..."
required
value={formData.message}
onChange={handleChange}
disabled={status === 'sending'}
/>
</div>
</div>
) : (
<div className="flex flex-col gap-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Floating Name */}
<div className="relative z-0 w-full group">
<input
type="text"
name="name"
id="name"
className="block py-3 px-0 w-full text-base text-foreground-custom bg-transparent border-0 border-b-2 border-border-hover/30 appearance-none focus:outline-none focus:ring-0 focus:border-primary-custom peer transition-colors"
placeholder=" "
required
value={formData.name}
onChange={handleChange}
disabled={status === 'sending'}
/>
<label
htmlFor="name"
className="peer-focus:font-medium absolute text-sm text-muted-foreground-custom duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:left-0 peer-focus:text-primary-custom peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
>
Full Name
</label>
</div>
{/* Floating Email */}
<div className="relative z-0 w-full group">
<input
type="email"
name="email"
id="email"
className="block py-3 px-0 w-full text-base text-foreground-custom bg-transparent border-0 border-b-2 border-border-hover/30 appearance-none focus:outline-none focus:ring-0 focus:border-primary-custom peer transition-colors"
placeholder=" "
required
value={formData.email}
onChange={handleChange}
disabled={status === 'sending'}
/>
<label
htmlFor="email"
className="peer-focus:font-medium absolute text-sm text-muted-foreground-custom duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:left-0 peer-focus:text-primary-custom peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
>
Email Address
</label>
</div>
</div>
{/* Floating Message */}
<div className="relative z-0 w-full group mt-2">
<textarea
name="message"
id="message"
rows={4}
className="block py-3 px-0 w-full text-base text-foreground-custom bg-transparent border-0 border-b-2 border-border-hover/30 appearance-none focus:outline-none focus:ring-0 focus:border-primary-custom peer transition-colors resize-none"
placeholder=" "
required
value={formData.message}
onChange={handleChange}
disabled={status === 'sending'}
/>
<label
htmlFor="message"
className="peer-focus:font-medium absolute text-sm text-muted-foreground-custom duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:left-0 peer-focus:text-primary-custom peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
>
How can I help you?
</label>
</div>
</div>
)}
{/* Submit Button */}
<button
type="submit"
disabled={status === 'sending'}
className="group relative w-full sm:w-auto self-end mt-4 px-8 py-4 rounded-full bg-primary-custom text-white font-bold text-sm shadow-xl shadow-primary-custom/20 hover:shadow-primary-custom/40 hover:scale-[1.02] active:scale-[0.98] transition-all duration-300 overflow-hidden disabled:opacity-70 disabled:cursor-not-allowed flex items-center justify-center gap-3"
className={`group relative w-full sm:w-auto self-end mt-2 px-8 py-3.5 rounded-full bg-primary-custom text-white font-bold text-sm shadow-md hover:shadow-lg transition-all duration-200 overflow-hidden disabled:opacity-70 disabled:cursor-not-allowed flex items-center justify-center gap-3 cursor-pointer ${
isLowTier ? 'hover:scale-100 active:scale-100 rounded-md' : 'hover:scale-[1.02] active:scale-[0.98]'
}`}
>
<span className="relative z-10 flex items-center gap-2">
{status === 'sending' ? 'Sending...' : 'Send Message'}
@@ -271,8 +351,9 @@ export default function ContactForm({
</svg>
)}
</span>
{/* Hover flare effect */}
<div className="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/20 to-transparent group-hover:animate-[shimmer_1.5s_infinite]" />
{!isLowTier && (
<div className="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/20 to-transparent group-hover:animate-[shimmer_1.5s_infinite]" />
)}
</button>
</motion.form>
)}
+11 -49
View File
@@ -101,30 +101,6 @@ export default function Skills({ skills, sectionTag = '03 // Tech Stack' }: Skil
const categories = Array.from(new Set(skills.map((s) => s.category || 'Core')));
// Bento span calculation to produce aesthetic varying card proportions
const getBentoSpan = (index: number, total: number) => {
if (total === 1) return 'lg:col-span-12';
if (total === 2) return 'lg:col-span-6';
if (total === 3) {
if (index === 0) return 'lg:col-span-12';
return 'lg:col-span-6';
}
if (total === 4) {
if (index === 0) return 'lg:col-span-7';
if (index === 1) return 'lg:col-span-5';
return 'lg:col-span-6';
}
if (total >= 5) {
if (index === 0) return 'lg:col-span-7 md:col-span-12';
if (index === 1) return 'lg:col-span-5 md:col-span-12';
if (index === 2) return 'lg:col-span-4 md:col-span-6';
if (index === 3) return 'lg:col-span-4 md:col-span-6';
if (index === 4) return 'lg:col-span-4 md:col-span-12';
return 'lg:col-span-6';
}
return 'lg:col-span-4';
};
return (
<section id="skills" className="py-20 relative">
<div className="max-w-6xl mx-auto px-4 sm:px-6 relative z-10">
@@ -144,18 +120,17 @@ export default function Skills({ skills, sectionTag = '03 // Tech Stack' }: Skil
</div>
</ScrollReveal>
{/* ─── Bento Box Grid for Tech Stack ─── */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-5">
{/* Clean, Easy-to-Read Categories Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{categories.map((category, idx) => {
const categorySkills = skills.filter((s) => (s.category || 'Core') === category);
const spanClass = getBentoSpan(idx, categories.length);
return (
<div key={category} className={`${spanClass} col-span-12`}>
<ScrollReveal variant="fade-up" delay={idx * 60} className="h-full">
<div className="bento-card p-6 sm:p-7 flex flex-col justify-between h-full group">
<div key={category} className="col-span-1">
<ScrollReveal variant="fade-up" delay={idx * 50} className="h-full">
<div className="bento-card p-5 sm:p-6 flex flex-col justify-between h-full group">
<div>
{/* Header */}
{/* Category Header */}
<div className="flex items-center justify-between pb-3 mb-4 border-b border-border-custom">
<div className="flex items-center gap-2.5">
<span className="p-1.5 rounded-lg bg-primary-custom/10 text-primary-custom">
@@ -170,32 +145,19 @@ export default function Skills({ skills, sectionTag = '03 // Tech Stack' }: Skil
</span>
</div>
{/* Skill Items List / Pills */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 my-2">
{/* Clean Skill Items without individual boxes */}
<div className="flex flex-wrap gap-x-5 gap-y-3 py-1">
{categorySkills.map((skill) => (
<div
key={skill.name}
className="p-3 rounded-xl border border-border-custom/80 bg-foreground-custom/[0.02] hover:bg-foreground-custom/[0.05] hover:border-primary-custom/40 transition-all flex items-center justify-between group/item"
className="flex items-center gap-2 text-xs font-medium text-foreground-custom/90 hover:text-primary-custom transition-colors"
>
<div className="flex items-center gap-2.5 min-w-0">
<span className="w-7 h-7 rounded-lg bg-primary-custom/10 text-primary-custom flex items-center justify-center text-sm shrink-0 group-hover/item:bg-primary-custom group-hover/item:text-white transition-all shadow-xs">
<i className={getSkillFaClass(skill)} />
</span>
<span className="text-xs font-semibold text-foreground-custom group-hover/item:text-primary-custom transition-colors truncate">
{skill.name}
</span>
</div>
<span className="w-1.5 h-1.5 rounded-full bg-primary-custom/40 group-hover/item:bg-primary-custom transition-colors shrink-0 ml-2" />
<i className={`${getSkillFaClass(skill)} text-primary-custom text-sm shrink-0`} />
<span>{skill.name}</span>
</div>
))}
</div>
</div>
{/* Bento Cell Foot */}
<div className="pt-3 mt-4 border-t border-border-custom flex items-center justify-between text-[10px] font-mono text-muted-foreground-custom">
<span>CATEGORY // {category.toUpperCase()}</span>
<span className="w-1.5 h-1.5 rounded-full bg-primary-custom/60" />
</div>
</div>
</ScrollReveal>
</div>
+20 -1
View File
@@ -373,7 +373,7 @@ html[data-tier='low'].dark .impact-pill {
color: #e5e7eb !important;
}
/* 7. Classic Form Inputs */
/* 7. Classic Form Inputs & Static Labels (No floating animations in low tier) */
html[data-tier='low'] input,
html[data-tier='low'] textarea,
html[data-tier='low'] select {
@@ -381,6 +381,25 @@ html[data-tier='low'] select {
box-shadow: none !important;
}
html[data-tier='low'] #contact label {
position: static !important;
transform: none !important;
display: block !important;
margin-bottom: 6px !important;
font-size: 12px !important;
font-weight: 600 !important;
color: var(--fg-color) !important;
opacity: 1 !important;
z-index: auto !important;
animation: none !important;
transition: none !important;
}
html[data-tier='low'] #contact .peer-focus\:-translate-y-6,
html[data-tier='low'] #contact [class*="translate-y"] {
transform: none !important;
}
html[data-tier='low']:not(.dark) input,
html[data-tier='low']:not(.dark) textarea,
html[data-tier='low']:not(.dark) select {