Compare commits
1 Commits
production
...
c847a456a3
| Author | SHA1 | Date | |
|---|---|---|---|
| c847a456a3 |
@@ -46,7 +46,7 @@ export default function BooksPage() {
|
|||||||
href={`/${locale}`}
|
href={`/${locale}`}
|
||||||
className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-10 group"
|
className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-10 group"
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
|
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform duration-200 ease-out" />
|
||||||
<span className="font-bold uppercase tracking-widest text-xs">{common("backToHome")}</span>
|
<span className="font-bold uppercase tracking-widest text-xs">{common("backToHome")}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<h1 className="text-6xl md:text-[10rem] font-black tracking-tighter text-stone-900 dark:text-stone-50 leading-[0.85] uppercase">
|
<h1 className="text-6xl md:text-[10rem] font-black tracking-tighter text-stone-900 dark:text-stone-50 leading-[0.85] uppercase">
|
||||||
@@ -76,7 +76,7 @@ export default function BooksPage() {
|
|||||||
) : (
|
) : (
|
||||||
reviews.map((review, i) => (
|
reviews.map((review, i) => (
|
||||||
<ScrollFadeIn key={review.id} delay={i * 0.1}>
|
<ScrollFadeIn key={review.id} delay={i * 0.1}>
|
||||||
<div className="bg-white dark:bg-stone-900 rounded-[2.5rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm flex flex-col h-full hover:shadow-xl transition-all">
|
<div className="bg-white dark:bg-stone-900 rounded-[2.5rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm flex flex-col h-full hover:shadow-xl transition-shadow duration-300 ease-out">
|
||||||
{review.book_image && (
|
{review.book_image && (
|
||||||
<div className="relative aspect-[3/4] rounded-2xl overflow-hidden mb-8 shadow-xl border-4 border-stone-50 dark:border-stone-800">
|
<div className="relative aspect-[3/4] rounded-2xl overflow-hidden mb-8 shadow-xl border-4 border-stone-50 dark:border-stone-800">
|
||||||
<Image src={review.book_image} alt={review.book_title} fill className="object-cover" sizes="(max-width: 768px) 100vw, 33vw" />
|
<Image src={review.book_image} alt={review.book_title} fill className="object-cover" sizes="(max-width: 768px) 100vw, 33vw" />
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default function ProjectDetailClient({
|
|||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-12 group bg-transparent border-none cursor-pointer"
|
className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-12 group bg-transparent border-none cursor-pointer"
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
|
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform duration-200 ease-out" />
|
||||||
<span className="font-bold uppercase tracking-widest text-xs">
|
<span className="font-bold uppercase tracking-widest text-xs">
|
||||||
{tCommon("back")}
|
{tCommon("back")}
|
||||||
</span>
|
</span>
|
||||||
@@ -127,11 +127,11 @@ export default function ProjectDetailClient({
|
|||||||
|
|
||||||
{project.live && project.live !== "#" && (
|
{project.live && project.live !== "#" && (
|
||||||
|
|
||||||
<a href={project.live} target="_blank" rel="noopener noreferrer" className="flex items-center justify-between w-full p-5 bg-white text-stone-900 rounded-2xl font-black hover:scale-105 transition-transform group">
|
<a href={project.live} target="_blank" rel="noopener noreferrer" className="flex items-center justify-between w-full p-5 bg-white text-stone-900 rounded-2xl font-black active:scale-[0.97] transition-transform duration-150 ease-out group">
|
||||||
|
|
||||||
<span>{project.button_live_label || tDetail("liveDemo")}</span>
|
<span>{project.button_live_label || tDetail("liveDemo")}</span>
|
||||||
|
|
||||||
<ExternalLink size={20} className="group-hover:translate-x-1 transition-transform" />
|
<ExternalLink size={20} className="group-hover:translate-x-1 transition-transform duration-200 ease-out" />
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export default function ProjectsPageClient({
|
|||||||
href={`/${locale}`}
|
href={`/${locale}`}
|
||||||
className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-10 group"
|
className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-10 group"
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
|
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform duration-200 ease-out" />
|
||||||
<span className="font-bold uppercase tracking-widest text-xs">{tCommon("backToHome")}</span>
|
<span className="font-bold uppercase tracking-widest text-xs">{tCommon("backToHome")}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ export default function ProjectsPageClient({
|
|||||||
<button
|
<button
|
||||||
key={cat}
|
key={cat}
|
||||||
onClick={() => setSelectedCategory(cat)}
|
onClick={() => setSelectedCategory(cat)}
|
||||||
className={`px-6 py-2 rounded-full text-[10px] font-black uppercase tracking-widest transition-all ${
|
className={`px-6 py-2 rounded-full text-[10px] font-black uppercase tracking-widest transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.97] ${
|
||||||
selectedCategory === cat
|
selectedCategory === cat
|
||||||
? "bg-stone-900 dark:bg-stone-100 text-white dark:text-stone-900"
|
? "bg-stone-900 dark:bg-stone-100 text-white dark:text-stone-900"
|
||||||
: "bg-white dark:bg-stone-900 text-stone-500 border border-stone-200 dark:border-stone-800"
|
: "bg-white dark:bg-stone-900 text-stone-500 border border-stone-200 dark:border-stone-800"
|
||||||
@@ -106,7 +106,7 @@ export default function ProjectsPageClient({
|
|||||||
placeholder={tList("searchPlaceholder")}
|
placeholder={tList("searchPlaceholder")}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="w-full bg-white dark:bg-stone-900 border border-stone-200 dark:border-stone-800 rounded-2xl py-4 pl-12 pr-6 focus:outline-none focus:ring-2 focus:ring-liquid-mint/30 transition-all shadow-sm"
|
className="w-full bg-white dark:bg-stone-900 border border-stone-200 dark:border-stone-800 rounded-2xl py-4 pl-12 pr-6 focus:outline-none focus:ring-2 focus:ring-liquid-mint/30 transition-[border-color,box-shadow,background-color] duration-150 ease-out shadow-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,12 +125,12 @@ export default function ProjectsPageClient({
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
filteredProjects.map((project) => (
|
filteredProjects.map((project) => (
|
||||||
<motion.div key={project.id} initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}>
|
<motion.div key={project.id} initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-60px" }} transition={{ duration: 0.5, ease: [0.23, 1, 0.32, 1] }}>
|
||||||
<Link href={`/${locale}/projects/${project.slug}`} className="group block h-full">
|
<Link href={`/${locale}/projects/${project.slug}`} className="group block h-full">
|
||||||
<div className="bg-white dark:bg-stone-900 rounded-[3rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm h-full hover:shadow-xl transition-all flex flex-col">
|
<div className="bg-white dark:bg-stone-900 rounded-[3rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm h-full hover:shadow-xl transition-shadow duration-300 ease-out flex flex-col">
|
||||||
{project.imageUrl ? (
|
{project.imageUrl ? (
|
||||||
<div className="relative aspect-[16/10] rounded-[2rem] overflow-hidden mb-8 border-4 border-stone-50 dark:border-stone-800 shadow-lg">
|
<div className="relative aspect-[16/10] rounded-[2rem] overflow-hidden mb-8 border-4 border-stone-50 dark:border-stone-800 shadow-lg">
|
||||||
<Image src={project.imageUrl} alt={project.title} fill className="object-cover group-hover:scale-105 transition-transform duration-700" />
|
<Image src={project.imageUrl} alt={project.title} fill className="object-cover group-hover:scale-[1.03] transition-transform duration-500 ease-out" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative aspect-[16/10] rounded-[2rem] overflow-hidden mb-8 border-4 border-stone-50 dark:border-stone-800 shadow-lg">
|
<div className="relative aspect-[16/10] rounded-[2rem] overflow-hidden mb-8 border-4 border-stone-50 dark:border-stone-800 shadow-lg">
|
||||||
@@ -146,7 +146,7 @@ export default function ProjectsPageClient({
|
|||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
<div className="flex justify-between items-start mb-4">
|
<div className="flex justify-between items-start mb-4">
|
||||||
<h3 className="text-3xl font-black text-stone-900 dark:text-white uppercase tracking-tight">{project.title}</h3>
|
<h3 className="text-3xl font-black text-stone-900 dark:text-white uppercase tracking-tight">{project.title}</h3>
|
||||||
<div className="w-12 h-12 rounded-full bg-stone-50 dark:bg-stone-800 flex items-center justify-center group-hover:bg-stone-900 dark:group-hover:bg-stone-50 group-hover:text-white dark:group-hover:text-stone-900 transition-all">
|
<div className="w-12 h-12 rounded-full bg-stone-50 dark:bg-stone-800 flex items-center justify-center group-hover:bg-stone-900 dark:group-hover:bg-stone-50 group-hover:text-white dark:group-hover:text-stone-900 transition-colors duration-200">
|
||||||
<ArrowUpRight size={20} />
|
<ArrowUpRight size={20} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ const About = () => {
|
|||||||
<h3 className="text-xl sm:text-2xl font-black text-stone-900 dark:text-stone-50 flex items-center gap-2 sm:gap-3 uppercase tracking-tighter">
|
<h3 className="text-xl sm:text-2xl font-black text-stone-900 dark:text-stone-50 flex items-center gap-2 sm:gap-3 uppercase tracking-tighter">
|
||||||
<BookOpen className="text-liquid-purple" size={24} /> {at("library")}
|
<BookOpen className="text-liquid-purple" size={24} /> {at("library")}
|
||||||
</h3>
|
</h3>
|
||||||
<Link href={`/${locale}/books`} className="group/link flex items-center gap-2 text-stone-900 dark:text-stone-100 font-black border-b-2 border-stone-900 dark:border-stone-100 pb-1 hover:opacity-70 transition-all">
|
<Link href={`/${locale}/books`} className="group/link flex items-center gap-2 text-stone-900 dark:text-stone-100 font-black border-b-2 border-stone-900 dark:border-stone-100 pb-1 hover:opacity-70 transition-opacity">
|
||||||
{at("viewAll")} <ArrowRight size={14} className="group-hover/link:translate-x-1 transition-transform" />
|
{at("viewAll")} <ArrowRight size={14} className="group-hover/link:translate-x-1 transition-transform" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -226,7 +226,7 @@ const About = () => {
|
|||||||
return (
|
return (
|
||||||
<div key={hobby.id} className="p-3 sm:p-4 md:p-6 bg-stone-50 dark:bg-stone-800 rounded-xl sm:rounded-2xl border border-stone-100 dark:border-stone-700 hover:border-liquid-mint transition-colors group">
|
<div key={hobby.id} className="p-3 sm:p-4 md:p-6 bg-stone-50 dark:bg-stone-800 rounded-xl sm:rounded-2xl border border-stone-100 dark:border-stone-700 hover:border-liquid-mint transition-colors group">
|
||||||
<div className="flex items-center gap-2 sm:gap-3 mb-1.5 sm:mb-3">
|
<div className="flex items-center gap-2 sm:gap-3 mb-1.5 sm:mb-3">
|
||||||
<Icon size={16} className="text-liquid-mint group-hover:scale-110 transition-transform shrink-0 sm:w-5 sm:h-5" />
|
<Icon size={16} className="text-liquid-mint group-hover:scale-110 transition-transform duration-200 ease-out shrink-0 sm:w-5 sm:h-5" />
|
||||||
<h4 className="font-bold text-xs sm:text-sm text-stone-800 dark:text-stone-200 uppercase tracking-tight">{hobby.title}</h4>
|
<h4 className="font-bold text-xs sm:text-sm text-stone-800 dark:text-stone-200 uppercase tracking-tight">{hobby.title}</h4>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px] sm:text-xs text-stone-500 dark:text-stone-400 font-medium leading-relaxed hidden sm:block">
|
<p className="text-[11px] sm:text-xs text-stone-500 dark:text-stone-400 font-medium leading-relaxed hidden sm:block">
|
||||||
|
|||||||
@@ -130,8 +130,8 @@ export default function ActivityFeed({
|
|||||||
key={quoteIndex}
|
key={quoteIndex}
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: -10 }}
|
exit={{ opacity: 0, y: -6, filter: "blur(2px)", transition: { duration: 0.2, ease: [0.23, 1, 0.32, 1] } }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<p className="text-base sm:text-lg md:text-xl lg:text-2xl font-light leading-tight text-stone-400 dark:text-stone-400 italic">
|
<p className="text-base sm:text-lg md:text-xl lg:text-2xl font-light leading-tight text-stone-400 dark:text-stone-400 italic">
|
||||||
@@ -207,14 +207,14 @@ export default function ActivityFeed({
|
|||||||
{[0, 1, 2].map((i) => (
|
{[0, 1, 2].map((i) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={i}
|
key={i}
|
||||||
animate={{ height: ["20%", "100%", "20%"] }}
|
animate={{ scaleY: [0.2, 1, 0.2] }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 0.8,
|
duration: 0.8,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
delay: i * 0.2,
|
delay: i * 0.2,
|
||||||
ease: "easeInOut"
|
ease: "easeInOut"
|
||||||
}}
|
}}
|
||||||
className="w-[2px] bg-[#1DB954] rounded-full"
|
className="w-[2px] h-full origin-bottom bg-[#1DB954] rounded-full"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -225,12 +225,12 @@ export default function ActivityFeed({
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex gap-4 relative z-10"
|
className="flex gap-4 relative z-10"
|
||||||
>
|
>
|
||||||
<div className="w-16 h-16 rounded-lg overflow-hidden shrink-0 shadow-md relative group-hover:shadow-xl transition-shadow duration-500">
|
<div className="w-16 h-16 rounded-lg overflow-hidden shrink-0 shadow-md relative group-hover:shadow-xl transition-shadow duration-300">
|
||||||
<Image
|
<Image
|
||||||
src={data.music.albumArt}
|
src={data.music.albumArt}
|
||||||
alt="Album Art"
|
alt="Album Art"
|
||||||
fill
|
fill
|
||||||
className="object-cover transition-transform duration-700 group-hover:scale-110"
|
className="object-cover transition-transform duration-500 ease-out group-hover:scale-105"
|
||||||
sizes="64px"
|
sizes="64px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -103,9 +103,9 @@ export default function BentoChat() {
|
|||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === "Enter" && handleSend()}
|
onKeyDown={(e) => e.key === "Enter" && handleSend()}
|
||||||
placeholder="Ask me..."
|
placeholder="Ask me..."
|
||||||
className="w-full bg-white dark:bg-stone-800 border border-stone-200 dark:border-stone-700 rounded-2xl py-3 pl-4 pr-12 text-sm focus:outline-none focus:ring-2 focus:ring-liquid-purple/30 transition-all shadow-inner dark:text-white"
|
className="w-full bg-white dark:bg-stone-800 border border-stone-200 dark:border-stone-700 rounded-2xl py-3 pl-4 pr-12 text-sm focus:outline-none focus:ring-2 focus:ring-liquid-purple/30 transition-[border-color,box-shadow,background-color] duration-150 ease-out shadow-inner dark:text-white"
|
||||||
/>
|
/>
|
||||||
<button onClick={handleSend} aria-label="Send message" className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-liquid-purple hover:scale-110 transition-transform">
|
<button onClick={handleSend} aria-label="Send message" className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-liquid-purple active:scale-[0.97] transition-transform duration-150 ease-out">
|
||||||
<Send size={18} />
|
<Send size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -287,23 +287,23 @@ export default function ChatWidget() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
initial={{ scale: 0, opacity: 0 }}
|
initial={{ scale: 0.9, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1, transition: { duration: 0.25, ease: [0.23, 1, 0.32, 1] } }}
|
||||||
exit={{ scale: 0, opacity: 0 }}
|
exit={{ scale: 0.9, opacity: 0, transition: { duration: 0.15, ease: [0.23, 1, 0.32, 1] } }}
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => setIsOpen(true)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="fixed bottom-4 left-4 md:bottom-6 md:left-6 z-30 bg-white/80 backdrop-blur-xl text-stone-900 p-3.5 rounded-full shadow-[0_10px_26px_rgba(41,37,36,0.16)] hover:bg-white hover:scale-105 transition-all duration-300 group cursor-pointer border border-white/60 ring-1 ring-white/30"
|
className="fixed bottom-4 left-4 md:bottom-6 md:left-6 z-30 bg-white/80 backdrop-blur-xl text-stone-900 p-3.5 rounded-full shadow-[0_10px_26px_rgba(41,37,36,0.16)] hover:bg-white active:scale-[0.97] transition-[background-color,transform] duration-150 ease-out group cursor-pointer border border-white/60 ring-1 ring-white/30"
|
||||||
aria-label="Open chat"
|
aria-label="Open chat"
|
||||||
>
|
>
|
||||||
<MessageCircle size={24} />
|
<MessageCircle size={24} />
|
||||||
<span className="absolute top-0 right-0 w-3 h-3 bg-green-500 rounded-full animate-pulse shadow-sm border-2 border-white" />
|
<span className="absolute top-0 right-0 w-3 h-3 bg-green-500 rounded-full animate-pulse shadow-sm border-2 border-white" />
|
||||||
|
|
||||||
{/* Tooltip */}
|
{/* Tooltip */}
|
||||||
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 px-3 py-1.5 bg-stone-900/90 text-stone-50 text-xs font-medium rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-[100] shadow-xl backdrop-blur-sm">
|
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 px-3 py-1.5 bg-stone-900/90 text-stone-50 text-xs font-medium rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-150 ease-out group-hover:delay-300 whitespace-nowrap pointer-events-none z-[100] shadow-xl backdrop-blur-sm">
|
||||||
Chat with AI
|
Chat with AI
|
||||||
</span>
|
</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -318,8 +318,8 @@ export default function ChatWidget() {
|
|||||||
initial={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(10px)" }}
|
initial={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(10px)" }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }}
|
animate={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }}
|
||||||
exit={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(10px)" }}
|
exit={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(10px)" }}
|
||||||
transition={{ type: "spring", damping: 30, stiffness: 400 }}
|
transition={{ type: "spring", duration: 0.35, bounce: 0 }}
|
||||||
className="fixed bottom-20 left-4 right-4 md:bottom-24 md:left-6 md:right-auto z-30 md:w-[380px] h-[60vh] md:h-[550px] max-h-[600px] bg-white/80 backdrop-blur-xl saturate-100 rounded-2xl shadow-[0_12px_40px_rgba(41,37,36,0.16)] flex flex-col overflow-hidden border border-white/60 ring-1 ring-white/30"
|
className="origin-bottom-left fixed bottom-20 left-4 right-4 md:bottom-24 md:left-6 md:right-auto z-30 md:w-[380px] h-[60vh] md:h-[550px] max-h-[600px] bg-white/80 backdrop-blur-xl saturate-100 rounded-2xl shadow-[0_12px_40px_rgba(41,37,36,0.16)] flex flex-col overflow-hidden border border-white/60 ring-1 ring-white/30"
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-white/70 text-stone-900 p-4 flex items-center justify-between border-b border-white/50">
|
<div className="bg-white/70 text-stone-900 p-4 flex items-center justify-between border-b border-white/50">
|
||||||
@@ -450,12 +450,12 @@ export default function ChatWidget() {
|
|||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
placeholder="Ask anything..."
|
placeholder="Ask anything..."
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="flex-1 px-4 py-3 text-sm bg-[#f5f5f4] text-[#292524] rounded-xl border border-[#e7e5e4] focus:outline-none focus:ring-2 focus:ring-[#e7e5e4] focus:border-[#a8a29e] focus:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-[#78716c] transition-all shadow-inner"
|
className="flex-1 px-4 py-3 text-sm bg-[#f5f5f4] text-[#292524] rounded-xl border border-[#e7e5e4] focus:outline-none focus:ring-2 focus:ring-[#e7e5e4] focus:border-[#a8a29e] focus:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-[#78716c] transition-[border-color,box-shadow,background-color] duration-150 ease-out shadow-inner"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={!inputValue.trim() || isLoading}
|
disabled={!inputValue.trim() || isLoading}
|
||||||
className="p-3 bg-[#292524] text-[#fdfcf8] rounded-xl hover:bg-[#44403c] hover:shadow-lg hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 shadow-md flex items-center justify-center aspect-square"
|
className="p-3 bg-[#292524] text-[#fdfcf8] rounded-xl hover:bg-[#44403c] hover:shadow-lg active:scale-[0.97] transition-[background-color,box-shadow,transform] duration-150 ease-out disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100 shadow-md flex items-center justify-center aspect-square"
|
||||||
aria-label="Send message"
|
aria-label="Send message"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -480,7 +480,7 @@ export default function ChatWidget() {
|
|||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="px-3 py-1.5 text-xs font-medium bg-[#f5f5f4] text-[#57534e] rounded-lg hover:bg-[#e7e5e4] hover:text-[#292524] border border-[#e7e5e4] transition-all whitespace-nowrap disabled:opacity-50 flex-shrink-0 shadow-sm"
|
className="px-3 py-1.5 text-xs font-medium bg-[#f5f5f4] text-[#57534e] rounded-lg hover:bg-[#e7e5e4] hover:text-[#292524] border border-[#e7e5e4] transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.97] whitespace-nowrap disabled:opacity-50 flex-shrink-0 shadow-sm"
|
||||||
>
|
>
|
||||||
{suggestion}
|
{suggestion}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
import { MotionConfig } from "framer-motion";
|
||||||
import { ToastProvider } from "@/components/Toast";
|
import { ToastProvider } from "@/components/Toast";
|
||||||
import ErrorBoundary from "@/components/ErrorBoundary";
|
import ErrorBoundary from "@/components/ErrorBoundary";
|
||||||
import { ConsentProvider } from "./ConsentProvider";
|
import { ConsentProvider } from "./ConsentProvider";
|
||||||
@@ -30,9 +31,12 @@ export default function ClientProviders({
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ConsentProvider>
|
<ConsentProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<GatedProviders mounted={mounted}>
|
{/* Respect prefers-reduced-motion in every Framer Motion animation */}
|
||||||
{children}
|
<MotionConfig reducedMotion="user">
|
||||||
</GatedProviders>
|
<GatedProviders mounted={mounted}>
|
||||||
|
{children}
|
||||||
|
</GatedProviders>
|
||||||
|
</MotionConfig>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</ConsentProvider>
|
</ConsentProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
+10
-10
@@ -200,7 +200,7 @@ const Contact = () => {
|
|||||||
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-stone-600 dark:text-stone-400 mb-1">Email</span>
|
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-stone-600 dark:text-stone-400 mb-1">Email</span>
|
||||||
<span className="text-lg sm:text-xl md:text-2xl font-black text-stone-900 dark:text-stone-50 tracking-tighter uppercase group-hover/link:text-liquid-mint transition-colors">contact@dk0.dev</span>
|
<span className="text-lg sm:text-xl md:text-2xl font-black text-stone-900 dark:text-stone-50 tracking-tighter uppercase group-hover/link:text-liquid-mint transition-colors">contact@dk0.dev</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-10 h-10 rounded-full border border-stone-100 dark:border-stone-800 flex items-center justify-center group-hover/link:bg-stone-900 dark:group-hover/link:bg-stone-50 group-hover/link:text-white dark:group-hover/link:text-stone-900 transition-all">
|
<div className="w-10 h-10 rounded-full border border-stone-100 dark:border-stone-800 flex items-center justify-center group-hover/link:bg-stone-900 dark:group-hover/link:bg-stone-50 group-hover/link:text-white dark:group-hover/link:text-stone-900 transition-colors duration-200">
|
||||||
<Mail size={16} />
|
<Mail size={16} />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -213,7 +213,7 @@ const Contact = () => {
|
|||||||
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-stone-600 dark:text-stone-400 mb-1">Code</span>
|
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-stone-600 dark:text-stone-400 mb-1">Code</span>
|
||||||
<span className="text-lg sm:text-xl md:text-2xl font-black text-stone-900 dark:text-stone-50 tracking-tighter uppercase group-hover/link:text-liquid-mint transition-colors">GitHub</span>
|
<span className="text-lg sm:text-xl md:text-2xl font-black text-stone-900 dark:text-stone-50 tracking-tighter uppercase group-hover/link:text-liquid-mint transition-colors">GitHub</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-10 h-10 rounded-full border border-stone-100 dark:border-stone-800 flex items-center justify-center group-hover/link:bg-stone-900 dark:group-hover/link:bg-stone-50 group-hover/link:text-white dark:group-hover/link:text-stone-900 transition-all">
|
<div className="w-10 h-10 rounded-full border border-stone-100 dark:border-stone-800 flex items-center justify-center group-hover/link:bg-stone-900 dark:group-hover/link:bg-stone-50 group-hover/link:text-white dark:group-hover/link:text-stone-900 transition-colors duration-200">
|
||||||
<Github size={16} />
|
<Github size={16} />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -226,7 +226,7 @@ const Contact = () => {
|
|||||||
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-stone-600 dark:text-stone-400 mb-1">Professional</span>
|
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-stone-600 dark:text-stone-400 mb-1">Professional</span>
|
||||||
<span className="text-lg sm:text-xl md:text-2xl font-black text-stone-900 dark:text-stone-50 tracking-tighter uppercase group-hover/link:text-[#0077b5] transition-colors">LinkedIn</span>
|
<span className="text-lg sm:text-xl md:text-2xl font-black text-stone-900 dark:text-stone-50 tracking-tighter uppercase group-hover/link:text-[#0077b5] transition-colors">LinkedIn</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-10 h-10 rounded-full border border-stone-100 dark:border-stone-800 flex items-center justify-center group-hover/link:bg-stone-900 dark:group-hover/link:bg-stone-50 group-hover/link:text-white dark:group-hover/link:text-stone-900 transition-all">
|
<div className="w-10 h-10 rounded-full border border-stone-100 dark:border-stone-800 flex items-center justify-center group-hover/link:bg-stone-900 dark:group-hover/link:bg-stone-50 group-hover/link:text-white dark:group-hover/link:text-stone-900 transition-colors duration-200">
|
||||||
<Linkedin size={16} />
|
<Linkedin size={16} />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -266,7 +266,7 @@ const Contact = () => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
required
|
required
|
||||||
className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-all font-medium"
|
className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-[border-color,box-shadow,background-color] duration-150 ease-out font-medium"
|
||||||
placeholder={tForm("placeholders.name")}
|
placeholder={tForm("placeholders.name")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -283,7 +283,7 @@ const Contact = () => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
required
|
required
|
||||||
className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-all font-medium"
|
className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-[border-color,box-shadow,background-color] duration-150 ease-out font-medium"
|
||||||
placeholder={tForm("placeholders.email")}
|
placeholder={tForm("placeholders.email")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -301,7 +301,7 @@ const Contact = () => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
required
|
required
|
||||||
className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-all font-medium"
|
className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-[border-color,box-shadow,background-color] duration-150 ease-out font-medium"
|
||||||
placeholder={tForm("placeholders.subject")}
|
placeholder={tForm("placeholders.subject")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -318,7 +318,7 @@ const Contact = () => {
|
|||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
required
|
required
|
||||||
rows={5}
|
rows={5}
|
||||||
className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-all font-medium resize-none"
|
className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-[border-color,box-shadow,background-color] duration-150 ease-out font-medium resize-none"
|
||||||
placeholder={tForm("placeholders.message")}
|
placeholder={tForm("placeholders.message")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -326,9 +326,9 @@ const Contact = () => {
|
|||||||
<motion.button
|
<motion.button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
whileHover={{ scale: 1.01 }}
|
whileTap={{ scale: 0.97 }}
|
||||||
whileTap={{ scale: 0.99 }}
|
transition={{ duration: 0.15, ease: [0.23, 1, 0.32, 1] }}
|
||||||
className="w-full py-4 sm:py-5 bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 rounded-xl sm:rounded-2xl font-black text-xs uppercase tracking-[0.2em] sm:tracking-[0.3em] flex items-center justify-center gap-3 shadow-xl hover:shadow-2xl transition-all disabled:opacity-50"
|
className="w-full py-4 sm:py-5 bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 rounded-xl sm:rounded-2xl font-black text-xs uppercase tracking-[0.2em] sm:tracking-[0.3em] flex items-center justify-center gap-3 shadow-xl hover:shadow-2xl transition-shadow duration-200 ease-out disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
|||||||
@@ -98,12 +98,8 @@ const CurrentlyReading = () => {
|
|||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
transition={{ duration: 0.6, delay: index * 0.1, ease: [0.25, 0.1, 0.25, 1] }}
|
transition={{ duration: 0.5, delay: index * 0.06, ease: [0.23, 1, 0.32, 1] }}
|
||||||
whileHover={{
|
className="relative overflow-hidden bg-gradient-to-br from-liquid-lavender/15 via-liquid-pink/10 to-liquid-rose/15 dark:from-stone-800 dark:via-stone-800 dark:to-stone-700 border-2 border-liquid-lavender/30 dark:border-stone-700 rounded-xl p-6 backdrop-blur-sm hover:border-liquid-lavender/50 dark:hover:border-stone-600 hover:from-liquid-lavender/20 hover:via-liquid-pink/15 hover:to-liquid-rose/20 transition-colors duration-300"
|
||||||
scale: 1.02,
|
|
||||||
transition: { duration: 0.4, ease: "easeOut" },
|
|
||||||
}}
|
|
||||||
className="relative overflow-hidden bg-gradient-to-br from-liquid-lavender/15 via-liquid-pink/10 to-liquid-rose/15 dark:from-stone-800 dark:via-stone-800 dark:to-stone-700 border-2 border-liquid-lavender/30 dark:border-stone-700 rounded-xl p-6 backdrop-blur-sm hover:border-liquid-lavender/50 dark:hover:border-stone-600 hover:from-liquid-lavender/20 hover:via-liquid-pink/15 hover:to-liquid-rose/20 transition-all duration-500 ease-out"
|
|
||||||
>
|
>
|
||||||
{/* Background Blob Animation */}
|
{/* Background Blob Animation */}
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -124,9 +120,9 @@ const CurrentlyReading = () => {
|
|||||||
{/* Book Cover */}
|
{/* Book Cover */}
|
||||||
{book.image && (
|
{book.image && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.5, delay: 0.2 + index * 0.1 }}
|
transition={{ duration: 0.4, delay: 0.1 + index * 0.06, ease: [0.23, 1, 0.32, 1] }}
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
>
|
>
|
||||||
<div className="relative w-24 h-36 sm:w-28 sm:h-40 rounded-lg overflow-hidden shadow-lg border-2 border-white/50 dark:border-stone-600">
|
<div className="relative w-24 h-36 sm:w-28 sm:h-40 rounded-lg overflow-hidden shadow-lg border-2 border-white/50 dark:border-stone-600">
|
||||||
@@ -162,11 +158,12 @@ const CurrentlyReading = () => {
|
|||||||
<span className="font-semibold">{isNaN(book.progress) ? 0 : book.progress}%</span>
|
<span className="font-semibold">{isNaN(book.progress) ? 0 : book.progress}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative h-2 bg-white/50 dark:bg-stone-700 rounded-full overflow-hidden border border-white/70 dark:border-stone-600">
|
<div className="relative h-2 bg-white/50 dark:bg-stone-700 rounded-full overflow-hidden border border-white/70 dark:border-stone-600">
|
||||||
|
{/* scaleX instead of width: stays on the compositor, no layout per frame */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ width: 0 }}
|
initial={{ scaleX: 0 }}
|
||||||
animate={{ width: `${isNaN(book.progress) ? 0 : book.progress}%` }}
|
animate={{ scaleX: (isNaN(book.progress) ? 0 : book.progress) / 100 }}
|
||||||
transition={{ duration: 1, delay: 0.3 + index * 0.1, ease: "easeOut" }}
|
transition={{ duration: 0.8, delay: 0.2 + index * 0.06, ease: [0.23, 1, 0.32, 1] }}
|
||||||
className="absolute left-0 top-0 h-full bg-gradient-to-r from-liquid-lavender via-liquid-pink to-liquid-rose rounded-full shadow-sm"
|
className="origin-left w-full absolute left-0 top-0 h-full bg-gradient-to-r from-liquid-lavender via-liquid-pink to-liquid-rose rounded-full shadow-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const Footer = () => {
|
|||||||
className="group flex flex-col items-center gap-4 text-stone-600 dark:text-stone-400 hover:text-stone-900 dark:hover:text-white transition-colors"
|
className="group flex flex-col items-center gap-4 text-stone-600 dark:text-stone-400 hover:text-stone-900 dark:hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
<span className="text-[10px] font-black uppercase tracking-[0.3em] text-stone-600 dark:text-stone-400" style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}>{t("backToTop")}</span>
|
<span className="text-[10px] font-black uppercase tracking-[0.3em] text-stone-600 dark:text-stone-400" style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}>{t("backToTop")}</span>
|
||||||
<div className="w-12 h-12 rounded-full border border-stone-200 dark:border-stone-800 flex items-center justify-center group-hover:bg-stone-900 dark:group-hover:bg-stone-50 group-hover:text-white dark:group-hover:text-stone-900 transition-all shadow-sm">
|
<div className="w-12 h-12 rounded-full border border-stone-200 dark:border-stone-800 flex items-center justify-center group-hover:bg-stone-900 dark:group-hover:bg-stone-50 group-hover:text-white dark:group-hover:text-stone-900 group-active:scale-[0.97] transition-[background-color,color,transform] duration-200 ease-out shadow-sm">
|
||||||
<ArrowUp size={20} />
|
<ArrowUp size={20} />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const Header = () => {
|
|||||||
{/* Logo Pill */}
|
{/* Logo Pill */}
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}`}
|
href={`/${locale}`}
|
||||||
className="w-10 h-10 flex items-center justify-center rounded-full bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 transition-transform hover:scale-105 active:scale-95 shadow-lg"
|
className="w-10 h-10 flex items-center justify-center rounded-full bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 transition-transform duration-150 ease-out active:scale-[0.97] shadow-lg"
|
||||||
>
|
>
|
||||||
<span className="font-black text-xs tracking-tighter">dk0</span>
|
<span className="font-black text-xs tracking-tighter">dk0</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -40,7 +40,7 @@ const Header = () => {
|
|||||||
<Link
|
<Link
|
||||||
key={item.name}
|
key={item.name}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="px-5 py-2 text-[10px] font-black uppercase tracking-[0.2em] text-stone-500 hover:text-stone-900 dark:hover:text-stone-100 rounded-full transition-all"
|
className="px-5 py-2 text-[10px] font-black uppercase tracking-[0.2em] text-stone-500 hover:text-stone-900 dark:hover:text-stone-100 rounded-full transition-colors"
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -62,7 +62,7 @@ const Header = () => {
|
|||||||
{/* Mobile Menu Toggle */}
|
{/* Mobile Menu Toggle */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className="w-8 h-8 flex md:hidden items-center justify-center text-stone-600 dark:text-stone-400 hover:bg-white dark:hover:bg-stone-800 rounded-full transition-colors shadow-sm"
|
className="w-8 h-8 flex md:hidden items-center justify-center text-stone-600 dark:text-stone-400 hover:bg-white dark:hover:bg-stone-800 rounded-full transition-[background-color,transform] duration-150 ease-out active:scale-[0.97] shadow-sm"
|
||||||
>
|
>
|
||||||
{isOpen ? <X size={14} /> : <Menu size={14} />}
|
{isOpen ? <X size={14} /> : <Menu size={14} />}
|
||||||
</button>
|
</button>
|
||||||
@@ -72,10 +72,10 @@ const Header = () => {
|
|||||||
|
|
||||||
{/* Mobile Menu Overlay */}
|
{/* Mobile Menu Overlay */}
|
||||||
<div
|
<div
|
||||||
className={`fixed top-24 left-6 right-6 z-40 bg-white/90 dark:bg-stone-900/95 backdrop-blur-3xl border border-white/40 dark:border-white/10 rounded-[2.5rem] shadow-2xl p-6 md:hidden overflow-hidden transition-all duration-200 ${
|
className={`fixed top-24 left-6 right-6 z-40 bg-white/90 dark:bg-stone-900/95 backdrop-blur-3xl border border-white/40 dark:border-white/10 rounded-[2.5rem] shadow-2xl p-6 md:hidden overflow-hidden origin-top transition-[opacity,transform] duration-200 ease-out ${
|
||||||
isOpen
|
isOpen
|
||||||
? "opacity-100 translate-y-0 pointer-events-auto"
|
? "opacity-100 translate-y-0 scale-100 pointer-events-auto"
|
||||||
: "opacity-0 -translate-y-2 pointer-events-none"
|
: "opacity-0 -translate-y-2 scale-[0.97] pointer-events-none"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
|||||||
@@ -50,10 +50,19 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
|||||||
setScrolled(window.scrollY > 50);
|
setScrolled(window.scrollY > 50);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("scroll", handleScroll);
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||||
return () => window.removeEventListener("scroll", handleScroll);
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") setIsOpen(false);
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", onKeyDown);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ name: translations.home, href: `/${locale}` },
|
{ name: translations.home, href: `/${locale}` },
|
||||||
{ name: translations.about, href: isHome ? "#about" : `/${locale}#about` },
|
{ name: translations.about, href: isHome ? "#about" : `/${locale}#about` },
|
||||||
@@ -81,18 +90,18 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
|||||||
<>
|
<>
|
||||||
<header className="fixed top-6 left-0 right-0 z-50 flex justify-center px-4 pointer-events-none">
|
<header className="fixed top-6 left-0 right-0 z-50 flex justify-center px-4 pointer-events-none">
|
||||||
<div
|
<div
|
||||||
className={`pointer-events-auto transition-all duration-500 ease-out ${
|
className={`pointer-events-auto transition-[max-width] duration-500 ease-in-out ${
|
||||||
scrolled ? "w-full max-w-5xl" : "w-full max-w-7xl"
|
scrolled ? "w-full max-w-5xl" : "w-full max-w-7xl"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`backdrop-blur-xl transition-all duration-500 flex justify-between items-center ${
|
className={`backdrop-blur-xl transition-[padding,background-color,box-shadow,border-color] duration-500 ease-in-out flex justify-between items-center ${
|
||||||
scrolled
|
scrolled
|
||||||
? "bg-white/95 border border-stone-200/50 shadow-[0_8px_30px_rgba(0,0,0,0.12)] rounded-full px-6 py-3"
|
? "bg-white/95 border border-stone-200/50 shadow-[0_8px_30px_rgba(0,0,0,0.12)] rounded-full px-6 py-3"
|
||||||
: "bg-white/85 border border-stone-200/30 shadow-[0_4px_24px_rgba(0,0,0,0.08)] px-4 py-4 rounded-full"
|
: "bg-white/85 border border-stone-200/30 shadow-[0_4px_24px_rgba(0,0,0,0.08)] px-4 py-4 rounded-full"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2 hover:scale-105 transition-transform">
|
<div className="flex items-center space-x-2 active:scale-[0.97] transition-transform duration-150 ease-out">
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}`}
|
href={`/${locale}`}
|
||||||
className="text-2xl font-black font-sans text-stone-900 tracking-tighter liquid-hover flex items-center"
|
className="text-2xl font-black font-sans text-stone-900 tracking-tighter liquid-hover flex items-center"
|
||||||
@@ -103,7 +112,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
|||||||
|
|
||||||
<nav className="hidden md:flex items-center space-x-8">
|
<nav className="hidden md:flex items-center space-x-8">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<div key={item.name} className="hover:-translate-y-0.5 active:scale-95 transition-all">
|
<div key={item.name} className="active:scale-[0.97] transition-transform duration-150 ease-out">
|
||||||
<Link
|
<Link
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="text-stone-700 hover:text-stone-900 font-medium transition-colors relative liquid-hover"
|
className="text-stone-700 hover:text-stone-900 font-medium transition-colors relative liquid-hover"
|
||||||
@@ -117,7 +126,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
|||||||
<div className="flex items-center space-x-2 ml-4 pl-4 border-l border-stone-300">
|
<div className="flex items-center space-x-2 ml-4 pl-4 border-l border-stone-300">
|
||||||
<Link
|
<Link
|
||||||
href={enHref}
|
href={enHref}
|
||||||
className={`px-2 py-1 text-sm font-medium rounded transition-all ${
|
className={`px-2 py-1 text-sm font-medium rounded transition-colors ${
|
||||||
locale === "en"
|
locale === "en"
|
||||||
? "bg-stone-900 text-white"
|
? "bg-stone-900 text-white"
|
||||||
: "text-stone-600 hover:text-stone-900 hover:bg-stone-100"
|
: "text-stone-600 hover:text-stone-900 hover:bg-stone-100"
|
||||||
@@ -127,7 +136,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
|||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={deHref}
|
href={deHref}
|
||||||
className={`px-2 py-1 text-sm font-medium rounded transition-all ${
|
className={`px-2 py-1 text-sm font-medium rounded transition-colors ${
|
||||||
locale === "de"
|
locale === "de"
|
||||||
? "bg-stone-900 text-white"
|
? "bg-stone-900 text-white"
|
||||||
: "text-stone-600 hover:text-stone-900 hover:bg-stone-100"
|
: "text-stone-600 hover:text-stone-900 hover:bg-stone-100"
|
||||||
@@ -141,7 +150,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className="md:hidden p-2 rounded-lg bg-stone-100 hover:bg-stone-200 text-stone-700 transition-all hover:scale-105 active:scale-95"
|
className="md:hidden p-2 rounded-lg bg-stone-100 hover:bg-stone-200 text-stone-700 transition-[background-color,transform] duration-150 ease-out active:scale-[0.97]"
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
>
|
>
|
||||||
{isOpen ? <XIcon size={24} /> : <MenuIcon size={24} />}
|
{isOpen ? <XIcon size={24} /> : <MenuIcon size={24} />}
|
||||||
@@ -150,19 +159,24 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Mobile menu overlay */}
|
{/* Mobile menu overlay — always mounted so it can fade out, not just vanish */}
|
||||||
{isOpen && (
|
<div
|
||||||
<div
|
aria-hidden="true"
|
||||||
className="fixed inset-0 bg-stone-900/50 backdrop-blur-sm z-40 md:hidden"
|
className={`fixed inset-0 bg-stone-900/50 backdrop-blur-sm z-40 md:hidden transition-opacity ${
|
||||||
onClick={() => setIsOpen(false)}
|
isOpen ? "opacity-100 duration-300 ease-out" : "opacity-0 pointer-events-none duration-200 ease-out"
|
||||||
/>
|
}`}
|
||||||
)}
|
onClick={() => setIsOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Mobile menu panel */}
|
{/* Mobile menu panel — drawer curve in, quicker exit */}
|
||||||
{isOpen && (
|
<div
|
||||||
<div
|
inert={!isOpen}
|
||||||
className="fixed right-0 top-0 h-full w-80 bg-white shadow-2xl z-50 md:hidden overflow-y-auto"
|
className={`fixed right-0 top-0 h-full w-80 max-w-[85vw] bg-white shadow-2xl z-50 md:hidden overflow-y-auto overscroll-contain transition-[transform,visibility] ${
|
||||||
>
|
isOpen
|
||||||
|
? "translate-x-0 visible duration-500 ease-drawer"
|
||||||
|
: "translate-x-full invisible duration-200 ease-out"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex justify-between items-center mb-8">
|
<div className="flex justify-between items-center mb-8">
|
||||||
<Link
|
<Link
|
||||||
@@ -174,7 +188,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
|||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
className="p-2 rounded-lg bg-stone-100 hover:bg-stone-200 text-stone-700"
|
className="p-2 rounded-lg bg-stone-100 hover:bg-stone-200 text-stone-700 transition-[background-color,transform] duration-150 ease-out active:scale-[0.97]"
|
||||||
aria-label="Close menu"
|
aria-label="Close menu"
|
||||||
>
|
>
|
||||||
<XIcon size={24} />
|
<XIcon size={24} />
|
||||||
@@ -187,7 +201,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
|||||||
key={item.name}
|
key={item.name}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
className="block px-4 py-3 text-stone-700 hover:bg-stone-50 rounded-lg font-medium transition-colors"
|
className="block px-4 py-3 text-stone-700 hover:bg-stone-50 active:bg-stone-100 rounded-lg font-medium transition-colors"
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -199,7 +213,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
|||||||
<Link
|
<Link
|
||||||
href={enHref}
|
href={enHref}
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
className={`flex-1 px-4 py-2 text-center font-medium rounded-lg transition-all ${
|
className={`flex-1 px-4 py-2 text-center font-medium rounded-lg transition-colors ${
|
||||||
locale === "en"
|
locale === "en"
|
||||||
? "bg-stone-900 text-white"
|
? "bg-stone-900 text-white"
|
||||||
: "bg-stone-100 text-stone-600 hover:bg-stone-200"
|
: "bg-stone-100 text-stone-600 hover:bg-stone-200"
|
||||||
@@ -210,7 +224,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
|||||||
<Link
|
<Link
|
||||||
href={deHref}
|
href={deHref}
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
className={`flex-1 px-4 py-2 text-center font-medium rounded-lg transition-all ${
|
className={`flex-1 px-4 py-2 text-center font-medium rounded-lg transition-colors ${
|
||||||
locale === "de"
|
locale === "de"
|
||||||
? "bg-stone-900 text-white"
|
? "bg-stone-900 text-white"
|
||||||
: "bg-stone-100 text-stone-600 hover:bg-stone-200"
|
: "bg-stone-100 text-stone-600 hover:bg-stone-200"
|
||||||
@@ -231,7 +245,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
|||||||
href={link.href}
|
href={link.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="p-3 rounded-lg bg-stone-100 hover:bg-stone-900 hover:text-white text-stone-700 transition-all"
|
className="p-3 rounded-lg bg-stone-100 hover:bg-stone-900 hover:text-white text-stone-700 transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.97]"
|
||||||
aria-label={link.label}
|
aria-label={link.label}
|
||||||
>
|
>
|
||||||
<Icon size={20} />
|
<Icon size={20} />
|
||||||
@@ -241,8 +255,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default async function Hero({ locale }: HeroProps) {
|
|||||||
|
|
||||||
{/* Left: Text Content */}
|
{/* Left: Text Content */}
|
||||||
<div className="flex-1 text-center xl:text-left space-y-6 sm:space-y-8 md:space-y-10">
|
<div className="flex-1 text-center xl:text-left space-y-6 sm:space-y-8 md:space-y-10">
|
||||||
<div className="inline-flex items-center gap-2 sm:gap-3 px-4 sm:px-5 py-2 sm:py-2.5 rounded-full bg-white dark:bg-stone-900 border border-stone-200 dark:border-stone-800 shadow-sm animate-[fadeIn_0.5s_ease-out]">
|
<div className="inline-flex items-center gap-2 sm:gap-3 px-4 sm:px-5 py-2 sm:py-2.5 rounded-full bg-white dark:bg-stone-900 border border-stone-200 dark:border-stone-800 shadow-sm animate-fade-in">
|
||||||
<span className="w-2 h-2 sm:w-2.5 sm:h-2.5 bg-emerald-500 rounded-full animate-pulse" />
|
<span className="w-2 h-2 sm:w-2.5 sm:h-2.5 bg-emerald-500 rounded-full animate-pulse" />
|
||||||
<span className="font-mono text-[10px] sm:text-[11px] font-black uppercase tracking-[0.2em] sm:tracking-[0.3em] text-stone-500">{t("badge")}</span>
|
<span className="font-mono text-[10px] sm:text-[11px] font-black uppercase tracking-[0.2em] sm:tracking-[0.3em] text-stone-500">{t("badge")}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,9 +39,9 @@ export default async function Hero({ locale }: HeroProps) {
|
|||||||
{t("description")}
|
{t("description")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-center gap-4 sm:gap-8 justify-center xl:justify-start pt-2 sm:pt-4 animate-[fadeIn_0.6s_ease-out_0.3s_both]">
|
<div className="flex flex-col sm:flex-row items-center gap-4 sm:gap-8 justify-center xl:justify-start pt-2 sm:pt-4 animate-fade-in [animation-delay:150ms]">
|
||||||
<a href="#projects" className="group relative px-8 sm:px-12 py-4 sm:py-5 bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 rounded-2xl sm:rounded-3xl font-black text-xs uppercase tracking-[0.2em] hover:scale-105 active:scale-95 transition-all shadow-2xl">
|
<a href="#projects" className="group relative overflow-hidden px-8 sm:px-12 py-4 sm:py-5 bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 rounded-2xl sm:rounded-3xl font-black text-xs uppercase tracking-[0.2em] active:scale-[0.97] transition-transform duration-150 ease-out shadow-2xl">
|
||||||
<div className="absolute inset-0 bg-white/10 -translate-x-full group-hover:translate-x-full transition-transform duration-1000" />
|
<div className="absolute inset-0 bg-white/10 -translate-x-full group-hover:translate-x-full transition-transform duration-700 ease-in-out" />
|
||||||
{t("ctaWork")}
|
{t("ctaWork")}
|
||||||
</a>
|
</a>
|
||||||
<a href="#contact" className="font-black text-xs uppercase tracking-[0.2em] text-stone-700 dark:text-stone-300 hover:text-stone-900 dark:hover:text-white transition-colors">
|
<a href="#contact" className="font-black text-xs uppercase tracking-[0.2em] text-stone-700 dark:text-stone-300 hover:text-stone-900 dark:hover:text-white transition-colors">
|
||||||
@@ -51,7 +51,7 @@ export default async function Hero({ locale }: HeroProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: The Photo */}
|
{/* Right: The Photo */}
|
||||||
<div className="relative w-52 h-52 sm:w-64 sm:h-64 md:w-80 md:h-80 xl:w-[500px] xl:h-[500px] shrink-0 mt-4 sm:mt-8 xl:mt-0 animate-[fadeIn_1s_ease-out]">
|
<div className="relative w-52 h-52 sm:w-64 sm:h-64 md:w-80 md:h-80 xl:w-[500px] xl:h-[500px] shrink-0 mt-4 sm:mt-8 xl:mt-0 animate-fade-in [animation-delay:100ms] [animation-duration:900ms]">
|
||||||
<div className="absolute inset-0 bg-gradient-to-tr from-liquid-mint to-liquid-purple rounded-[3rem] sm:rounded-[4rem] lg:rounded-[5rem] rotate-12 scale-90 opacity-20 blur-3xl" />
|
<div className="absolute inset-0 bg-gradient-to-tr from-liquid-mint to-liquid-purple rounded-[3rem] sm:rounded-[4rem] lg:rounded-[5rem] rotate-12 scale-90 opacity-20 blur-3xl" />
|
||||||
<div className="relative w-full h-full rounded-[2.5rem] sm:rounded-[3rem] lg:rounded-[4rem] overflow-hidden border-[12px] sm:border-[16px] lg:border-[24px] border-white dark:border-stone-900 shadow-[0_30px_60px_-15px_rgba(0,0,0,0.3)] sm:shadow-[0_50px_100px_-20px_rgba(0,0,0,0.4)]" style={{ willChange: "transform" }}>
|
<div className="relative w-full h-full rounded-[2.5rem] sm:rounded-[3rem] lg:rounded-[4rem] overflow-hidden border-[12px] sm:border-[16px] lg:border-[24px] border-white dark:border-stone-900 shadow-[0_30px_60px_-15px_rgba(0,0,0,0.3)] sm:shadow-[0_50px_100px_-20px_rgba(0,0,0,0.4)]" style={{ willChange: "transform" }}>
|
||||||
<Image src="/images/me.jpg" alt="Dennis Konkol" fill className="object-cover" priority fetchPriority="high" sizes="(max-width: 640px) 208px, (max-width: 768px) 256px, (max-width: 1280px) 320px, 500px" />
|
<Image src="/images/me.jpg" alt="Dennis Konkol" fill className="object-cover" priority fetchPriority="high" sizes="(max-width: 640px) 208px, (max-width: 768px) 256px, (max-width: 1280px) 320px, 500px" />
|
||||||
@@ -65,8 +65,8 @@ export default async function Hero({ locale }: HeroProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 hidden md:flex flex-col items-center gap-4 animate-bounce">
|
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 hidden md:flex flex-col items-center gap-4" aria-hidden="true">
|
||||||
<div className="w-px h-16 bg-gradient-to-b from-stone-300 dark:from-stone-700 to-transparent" />
|
<div className="w-px h-16 origin-top animate-scroll-hint bg-gradient-to-b from-stone-300 dark:from-stone-700 to-transparent" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ const Projects = () => {
|
|||||||
{t("subtitle")}
|
{t("subtitle")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link href={`/${locale}/projects`} className="group flex items-center gap-2 text-stone-900 dark:text-stone-100 font-black border-b-2 border-stone-900 dark:border-stone-100 pb-1 hover:opacity-70 transition-all text-xs uppercase tracking-widest">
|
<Link href={`/${locale}/projects`} className="group flex items-center gap-2 text-stone-900 dark:text-stone-100 font-black border-b-2 border-stone-900 dark:border-stone-100 pb-1 hover:opacity-70 transition-opacity text-xs uppercase tracking-widest">
|
||||||
{t("viewAll")} <ArrowUpRight className="group-hover:-translate-y-1 group-hover:translate-x-1 transition-transform" size={14} />
|
{t("viewAll")} <ArrowUpRight className="group-hover:-translate-y-0.5 group-hover:translate-x-0.5 transition-transform duration-200 ease-out" size={14} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ const Projects = () => {
|
|||||||
src={project.imageUrl}
|
src={project.imageUrl}
|
||||||
alt={project.title}
|
alt={project.title}
|
||||||
fill
|
fill
|
||||||
className="object-cover transition-transform duration-700 group-hover:scale-105"
|
className="object-cover transition-transform duration-500 ease-out group-hover:scale-[1.03]"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ProjectThumbnail
|
<ProjectThumbnail
|
||||||
@@ -106,7 +106,7 @@ const Projects = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Overlay on Hover */}
|
{/* Overlay on Hover */}
|
||||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-500" />
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-300" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Text Content */}
|
{/* Text Content */}
|
||||||
|
|||||||
@@ -136,15 +136,11 @@ const ReadBooks = () => {
|
|||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 0.6,
|
duration: 0.5,
|
||||||
delay: index * 0.1,
|
delay: index * 0.06,
|
||||||
ease: [0.25, 0.1, 0.25, 1],
|
ease: [0.23, 1, 0.32, 1],
|
||||||
}}
|
}}
|
||||||
whileHover={{
|
className="relative overflow-hidden bg-gradient-to-br from-liquid-mint/15 via-liquid-sky/10 to-liquid-teal/15 dark:from-stone-800 dark:via-stone-800 dark:to-stone-700 border-2 border-liquid-mint/30 dark:border-stone-700 rounded-xl p-5 backdrop-blur-sm hover:border-liquid-mint/50 dark:hover:border-stone-600 hover:from-liquid-mint/20 hover:via-liquid-sky/15 hover:to-liquid-teal/20 transition-colors duration-300"
|
||||||
scale: 1.02,
|
|
||||||
transition: { duration: 0.4, ease: "easeOut" },
|
|
||||||
}}
|
|
||||||
className="relative overflow-hidden bg-gradient-to-br from-liquid-mint/15 via-liquid-sky/10 to-liquid-teal/15 dark:from-stone-800 dark:via-stone-800 dark:to-stone-700 border-2 border-liquid-mint/30 dark:border-stone-700 rounded-xl p-5 backdrop-blur-sm hover:border-liquid-mint/50 dark:hover:border-stone-600 hover:from-liquid-mint/20 hover:via-liquid-sky/15 hover:to-liquid-teal/20 transition-all duration-500 ease-out"
|
|
||||||
>
|
>
|
||||||
{/* Background Blob */}
|
{/* Background Blob */}
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -165,9 +161,9 @@ const ReadBooks = () => {
|
|||||||
{/* Book Cover */}
|
{/* Book Cover */}
|
||||||
{review.book_image && (
|
{review.book_image && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.5, delay: 0.2 + index * 0.1 }}
|
transition={{ duration: 0.4, delay: 0.1 + index * 0.06, ease: [0.23, 1, 0.32, 1] }}
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
>
|
>
|
||||||
<div className="relative w-20 h-[7.5rem] sm:w-24 sm:h-32 rounded-lg overflow-hidden shadow-lg border-2 border-white/50 dark:border-stone-600">
|
<div className="relative w-20 h-[7.5rem] sm:w-24 sm:h-32 rounded-lg overflow-hidden shadow-lg border-2 border-white/50 dark:border-stone-600">
|
||||||
|
|||||||
@@ -18,9 +18,11 @@ export default function ScrollFadeIn({ children, className = "", delay = 0 }: Sc
|
|||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const [hasMounted, setHasMounted] = useState(false);
|
const [hasMounted, setHasMounted] = useState(false);
|
||||||
|
const [reduceMotion, setReduceMotion] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHasMounted(true);
|
setHasMounted(true);
|
||||||
|
setReduceMotion(window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
||||||
const el = ref.current;
|
const el = ref.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
@@ -37,7 +39,8 @@ export default function ScrollFadeIn({ children, className = "", delay = 0 }: Sc
|
|||||||
observer.unobserve(el);
|
observer.unobserve(el);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ threshold: 0.1 }
|
// Start a little before the section is fully in view so it never feels late
|
||||||
|
{ threshold: 0.1, rootMargin: "0px 0px -60px 0px" }
|
||||||
);
|
);
|
||||||
|
|
||||||
observer.observe(el);
|
observer.observe(el);
|
||||||
@@ -50,8 +53,8 @@ export default function ScrollFadeIn({ children, className = "", delay = 0 }: Sc
|
|||||||
className={className}
|
className={className}
|
||||||
style={hasMounted ? {
|
style={hasMounted ? {
|
||||||
opacity: isVisible ? 1 : 0,
|
opacity: isVisible ? 1 : 0,
|
||||||
transform: isVisible ? "translateY(0)" : "translateY(30px)",
|
transform: isVisible || reduceMotion ? "translateY(0)" : "translateY(16px)",
|
||||||
transition: `opacity 0.6s ease ${delay}s, transform 0.6s ease ${delay}s`,
|
transition: `opacity 0.6s var(--ease-out) ${delay}s, transform 0.6s var(--ease-out) ${delay}s`,
|
||||||
} : undefined}
|
} : undefined}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function ThemeToggle() {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={cycle}
|
onClick={cycle}
|
||||||
className="p-2 rounded-full bg-stone-100 dark:bg-stone-800 text-stone-800 dark:text-stone-100 hover:bg-stone-200 dark:hover:bg-stone-700 border border-stone-200 dark:border-stone-700 shadow-sm hover:scale-105 active:scale-95 transition-all"
|
className="p-2 rounded-full bg-stone-100 dark:bg-stone-800 text-stone-800 dark:text-stone-100 hover:bg-stone-200 dark:hover:bg-stone-700 border border-stone-200 dark:border-stone-700 shadow-sm active:scale-[0.97] transition-[background-color,transform] duration-150 ease-out"
|
||||||
aria-label={`Current theme: ${theme}. Click to switch.`}
|
aria-label={`Current theme: ${theme}. Click to switch.`}
|
||||||
title={theme === "system" ? "System" : theme === "dark" ? "Dark" : "Light"}
|
title={theme === "system" ? "System" : theme === "dark" ? "Dark" : "Light"}
|
||||||
>
|
>
|
||||||
|
|||||||
+26
-3
@@ -25,6 +25,11 @@
|
|||||||
--input: #efebe9;
|
--input: #efebe9;
|
||||||
--ring: #a1887f; /* Warm brown ring */
|
--ring: #a1887f; /* Warm brown ring */
|
||||||
--radius: 1rem;
|
--radius: 1rem;
|
||||||
|
|
||||||
|
/* Motion tokens: enter/exit = ease-out, on-screen movement = ease-in-out */
|
||||||
|
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
||||||
|
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -74,6 +79,20 @@ html {
|
|||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Reduced motion: drop movement, keep colour/opacity feedback */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
animation-delay: 0ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Liquid Glass Effects */
|
/* Liquid Glass Effects */
|
||||||
.glass-panel {
|
.glass-panel {
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
@@ -92,10 +111,13 @@ html {
|
|||||||
box-shadow:
|
box-shadow:
|
||||||
0 4px 6px -1px rgba(0, 0, 0, 0.06),
|
0 4px 6px -1px rgba(0, 0, 0, 0.06),
|
||||||
0 2px 4px -1px rgba(0, 0, 0, 0.05);
|
0 2px 4px -1px rgba(0, 0, 0, 0.05);
|
||||||
transition: all 0.6s cubic-bezier(0.25, 0.1, 0.25, 1);
|
transition:
|
||||||
will-change: transform, box-shadow;
|
transform 250ms var(--ease-out),
|
||||||
|
box-shadow 250ms var(--ease-out),
|
||||||
|
border-color 200ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) and (pointer: fine) {
|
||||||
.glass-card:hover {
|
.glass-card:hover {
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -104,6 +126,7 @@ html {
|
|||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
border-color: var(--ring);
|
border-color: var(--ring);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Typography & Headings */
|
/* Typography & Headings */
|
||||||
h1,
|
h1,
|
||||||
@@ -229,7 +252,7 @@ div {
|
|||||||
background: #fffcf5;
|
background: #fffcf5;
|
||||||
border: 1px solid #d7ccc8;
|
border: 1px solid #d7ccc8;
|
||||||
color: #3e2723;
|
color: #3e2723;
|
||||||
transition: all 0.2s ease;
|
transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
box-shadow: 0 1px 2px rgba(62, 39, 35, 0.05);
|
box-shadow: 0 1px 2px rgba(62, 39, 35, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-4
@@ -4,6 +4,11 @@ import type { Config } from "tailwindcss";
|
|||||||
// tailwind.config.js
|
// tailwind.config.js
|
||||||
export default {
|
export default {
|
||||||
darkMode: "class", // Enables class-based dark mode
|
darkMode: "class", // Enables class-based dark mode
|
||||||
|
// Touch devices fire :hover on tap and keep it stuck; only apply hover styles
|
||||||
|
// where a real pointer exists.
|
||||||
|
future: {
|
||||||
|
hoverOnlyWhenSupported: true,
|
||||||
|
},
|
||||||
content: [
|
content: [
|
||||||
"./app/**/*.{js,ts,jsx,tsx}",
|
"./app/**/*.{js,ts,jsx,tsx}",
|
||||||
"./app/components/**/*.{js,ts,jsx,tsx}",
|
"./app/components/**/*.{js,ts,jsx,tsx}",
|
||||||
@@ -11,19 +16,33 @@ export default {
|
|||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
// Stronger curves than the CSS defaults. Overriding `out` and `in-out`
|
||||||
|
// upgrades every existing `ease-out` / `ease-in-out` utility at once.
|
||||||
|
transitionTimingFunction: {
|
||||||
|
out: "cubic-bezier(0.23, 1, 0.32, 1)",
|
||||||
|
"in-out": "cubic-bezier(0.77, 0, 0.175, 1)",
|
||||||
|
drawer: "cubic-bezier(0.32, 0.72, 0, 1)",
|
||||||
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
fadeIn: {
|
fadeIn: {
|
||||||
"0%": { opacity: "0", transform: "translateY(10px)" },
|
"0%": { opacity: "0", transform: "translateY(8px)" },
|
||||||
"100%": { opacity: "1", transform: "translateY(0)" },
|
"100%": { opacity: "1", transform: "translateY(0)" },
|
||||||
},
|
},
|
||||||
slideDown: {
|
slideDown: {
|
||||||
"0%": { opacity: "0", transform: "translateY(-20px)" },
|
"0%": { opacity: "0", transform: "translateY(-12px)" },
|
||||||
"100%": { opacity: "1", transform: "translateY(0)" },
|
"100%": { opacity: "1", transform: "translateY(0)" },
|
||||||
},
|
},
|
||||||
|
scrollHint: {
|
||||||
|
"0%": { opacity: "0", transform: "scaleY(0.4)" },
|
||||||
|
"40%": { opacity: "1", transform: "scaleY(1)" },
|
||||||
|
"100%": { opacity: "0", transform: "scaleY(1)" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"fade-in": "fadeIn 0.5s ease-out",
|
"fade-in": "fadeIn 600ms cubic-bezier(0.23, 1, 0.32, 1) both",
|
||||||
"slide-down": "slideDown 0.4s ease-out",
|
"slide-down": "slideDown 500ms cubic-bezier(0.23, 1, 0.32, 1) both",
|
||||||
|
"scroll-hint": "scrollHint 2.4s cubic-bezier(0.77, 0, 0.175, 1) infinite",
|
||||||
|
"spin-slow": "spin 4s linear infinite",
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
background: "var(--background)",
|
background: "var(--background)",
|
||||||
|
|||||||
Reference in New Issue
Block a user