redesign
This commit is contained in:
@@ -0,0 +1,506 @@
|
||||
diff --git a/src/App.tsx b/src/App.tsx
|
||||
index 19c9751..78d9f0c 100644
|
||||
--- a/src/App.tsx
|
||||
+++ b/src/App.tsx
|
||||
@@ -5,7 +5,6 @@ import remarkGfm from "remark-gfm";
|
||||
|
||||
// Components
|
||||
import {
|
||||
- AnimatedBackground,
|
||||
ProjectThumb,
|
||||
CascadeItem,
|
||||
FadeContainer,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
|
||||
// Hooks
|
||||
import {
|
||||
- useSystemDarkMode,
|
||||
useGitHubReadme,
|
||||
useGitHubRepoImages,
|
||||
} from "./hooks";
|
||||
@@ -31,13 +29,27 @@ export default function PortfolioAboveTheFold() {
|
||||
null,
|
||||
);
|
||||
const [isContentVisible, setIsContentVisible] = useState(true);
|
||||
- const isDark = useSystemDarkMode();
|
||||
|
||||
// Handle project selection with fade transition
|
||||
const handleProjectSelect = (projectId: number) => {
|
||||
if (projectId === active) return;
|
||||
|
||||
- // Start fade out
|
||||
+ // If on mobile/tablet, trigger the modal instantly without waiting for fade-out
|
||||
+ if (window.innerWidth < 1024) {
|
||||
+ setIsContentVisible(false); // Reset content visibility for the cascade
|
||||
+ setActive(projectId);
|
||||
+ setDisplayedProject(projectId);
|
||||
+
|
||||
+ // Wait for the modal wrapper to begin its CSS transition before starting the cascade
|
||||
+ requestAnimationFrame(() => {
|
||||
+ requestAnimationFrame(() => {
|
||||
+ setIsContentVisible(true);
|
||||
+ });
|
||||
+ });
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ // Start fade out for desktop
|
||||
setIsContentVisible(false);
|
||||
|
||||
// After fade out completes, update the project and fade in
|
||||
@@ -48,19 +60,31 @@ export default function PortfolioAboveTheFold() {
|
||||
requestAnimationFrame(() => {
|
||||
setIsContentVisible(true);
|
||||
});
|
||||
- }, 200); // Match the fade-out duration
|
||||
+ }, 300); // Match the fade-out duration
|
||||
};
|
||||
|
||||
// Handle closing with fade transition
|
||||
const handleClose = () => {
|
||||
setIsContentVisible(false);
|
||||
+
|
||||
+ if (window.innerWidth < 1024) {
|
||||
+ // Instantly start scaling down the modal on mobile
|
||||
+ setActive(null);
|
||||
+ setDisplayedProject(null);
|
||||
+ requestAnimationFrame(() => {
|
||||
+ setIsContentVisible(true);
|
||||
+ });
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ // On desktop, wait for fade-out before resetting project
|
||||
setTimeout(() => {
|
||||
setActive(null);
|
||||
setDisplayedProject(null);
|
||||
requestAnimationFrame(() => {
|
||||
setIsContentVisible(true);
|
||||
});
|
||||
- }, 200);
|
||||
+ }, 300);
|
||||
};
|
||||
|
||||
// Combine all projects for lookup
|
||||
@@ -87,40 +111,36 @@ export default function PortfolioAboveTheFold() {
|
||||
} = useGitHubReadme(activeRepo);
|
||||
|
||||
return (
|
||||
- <main className="h-screen w-full flex items-center justify-center p-6 overflow-hidden relative">
|
||||
- <AnimatedBackground
|
||||
- palette={activeProject?.palette}
|
||||
- isDark={isDark}
|
||||
- />
|
||||
- <section className="relative max-w-[1100px] w-full h-[88vh] bg-white/60 dark:bg-slate-900/60 backdrop-blur-sm border border-white/60 dark:border-slate-700/60 rounded-2xl shadow-2xl p-6 grid grid-cols-2 gap-6 items-stretch">
|
||||
- <ScrollArea className="min-h-0" scrollbarOffset={12}>
|
||||
+ <main className="h-screen w-full bg-white dark:bg-slate-950 overflow-hidden font-sans text-slate-900 dark:text-slate-100 transition-colors duration-300">
|
||||
+ <section className="mx-auto max-w-[1400px] w-full h-full grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-stretch px-6 py-6 md:px-12 md:py-12 lg:px-16 lg:py-16 relative">
|
||||
+ <ScrollArea className="min-h-0 pr-4 lg:pr-6" scrollbarOffset={12}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
- <p className="text-sm uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
+ <p className="text-sm font-medium tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Hello, I'm
|
||||
</p>
|
||||
- <h1 className="mt-2 text-[clamp(22px,3.6vw,40px)] leading-tight font-semibold text-gray-900 dark:text-white">
|
||||
+ <h1 className="mt-2 text-[clamp(32px,5vw,56px)] leading-none font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
{profile.name}
|
||||
</h1>
|
||||
- <p className="mt-1 text-[clamp(14px,1.6vw,18px)] text-gray-600 dark:text-gray-300">
|
||||
- {profile.role} • {profile.location}
|
||||
+ <p className="mt-2 text-[clamp(16px,2vw,22px)] font-medium text-gray-600 dark:text-gray-300">
|
||||
+ {profile.role} <span className="text-gray-300 dark:text-gray-600 mx-2">•</span> {profile.location}
|
||||
</p>
|
||||
|
||||
- <p className="mt-6 text-[clamp(13px,1.2vw,16px)] text-gray-700 dark:text-gray-300 max-w-[38ch]">
|
||||
+ <p className="mt-8 text-base lg:text-lg text-gray-600 dark:text-gray-400 max-w-[45ch] leading-relaxed">
|
||||
{profile.blurb}
|
||||
</p>
|
||||
|
||||
- <div className="mt-6 flex flex-wrap gap-3">
|
||||
+ <div className="mt-8 flex flex-wrap gap-4">
|
||||
<a
|
||||
href={profile.cv}
|
||||
- className="inline-flex items-center gap-2 rounded-md px-4 py-2 border border-gray-200 dark:border-slate-600 shadow-sm text-sm font-medium text-gray-900 dark:text-white hover:shadow hover:-translate-y-0.5 transition-transform"
|
||||
+ className="inline-flex items-center gap-2 rounded-full px-6 py-2.5 border border-gray-200 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm text-sm font-medium text-gray-900 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
Download CV
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={`mailto:${profile.email}`}
|
||||
- className="inline-flex items-center gap-2 rounded-md px-4 py-2 bg-gray-900 dark:bg-white text-white dark:text-gray-900 text-sm font-medium hover:opacity-95 transition-opacity"
|
||||
+ className="inline-flex items-center gap-2 rounded-full px-6 py-2.5 bg-gray-900 dark:bg-white text-white dark:text-gray-900 text-sm font-medium hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
Contact
|
||||
</a>
|
||||
@@ -137,48 +157,24 @@ export default function PortfolioAboveTheFold() {
|
||||
const isActive = active === p.id;
|
||||
|
||||
return (
|
||||
- // Wrapper div creates the gradient border effect
|
||||
<div
|
||||
key={p.id}
|
||||
- className="group relative rounded-lg p-[2px] transition-all duration-200 shadow-sm hover:shadow-md hover:-translate-y-0.5 bg-white dark:bg-slate-800"
|
||||
+ className={`group relative rounded-xl transition-all duration-200 bg-gray-50 dark:bg-slate-900 border ${
|
||||
+ isActive
|
||||
+ ? "border-gray-900 dark:border-white shadow-sm"
|
||||
+ : "border-transparent hover:border-gray-200 dark:hover:border-slate-700"
|
||||
+ }`}
|
||||
>
|
||||
- {/* Gradient border layer - always present, opacity controlled */}
|
||||
- <div
|
||||
- aria-hidden
|
||||
- className="absolute inset-0 rounded-lg transition-opacity duration-300 pointer-events-none"
|
||||
- style={{
|
||||
- backgroundImage: p.gradient,
|
||||
- opacity: isActive ? 1 : 0,
|
||||
- }}
|
||||
- />
|
||||
<button
|
||||
onClick={() =>
|
||||
handleProjectSelect(p.id)
|
||||
}
|
||||
- className={`relative flex flex-col w-full rounded-[6px] overflow-hidden
|
||||
- bg-white dark:bg-slate-800
|
||||
- focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo-500`}
|
||||
+ className={`relative flex flex-col w-full rounded-xl overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500`}
|
||||
aria-expanded={isActive}
|
||||
type="button"
|
||||
>
|
||||
- {/* Gradient overlay that covers the entire button; becomes visible on hover. */}
|
||||
- <div
|
||||
- aria-hidden
|
||||
- className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none rounded-[6px]"
|
||||
- style={{
|
||||
- backgroundImage:
|
||||
- p.gradient,
|
||||
- }}
|
||||
- />
|
||||
-
|
||||
- {/* Slight dark layer to insure text contrast when gradient is visible */}
|
||||
- <div
|
||||
- aria-hidden
|
||||
- className="absolute inset-0 bg-black/0 group-hover:bg-black/25 transition-colors duration-300 pointer-events-none rounded-[6px]"
|
||||
- />
|
||||
-
|
||||
{/* Image fills width */}
|
||||
- <div className="relative z-10 w-full h-24 overflow-hidden">
|
||||
+ <div className="relative z-10 w-full h-32 overflow-hidden bg-gray-200 dark:bg-slate-800">
|
||||
<ProjectThumb
|
||||
src={
|
||||
(p.repo &&
|
||||
@@ -192,11 +188,11 @@ export default function PortfolioAboveTheFold() {
|
||||
</div>
|
||||
|
||||
{/* Text below image */}
|
||||
- <div className="relative z-10 text-left p-2">
|
||||
- <div className="text-sm font-medium transition-colors duration-200 text-gray-900 dark:text-white group-hover:text-white">
|
||||
+ <div className="relative z-10 text-left p-3">
|
||||
+ <div className="text-sm font-semibold transition-colors duration-200 text-gray-900 dark:text-white">
|
||||
{p.title}
|
||||
</div>
|
||||
- <div className="text-xs transition-colors duration-200 text-gray-500 dark:text-gray-400 group-hover:text-gray-100 line-clamp-2">
|
||||
+ <div className="text-xs transition-colors duration-200 text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">
|
||||
{p.desc}
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,48 +211,24 @@ export default function PortfolioAboveTheFold() {
|
||||
const isActive = active === p.id;
|
||||
|
||||
return (
|
||||
- // Wrapper div creates the gradient border effect
|
||||
<div
|
||||
key={p.id}
|
||||
- className="group relative rounded-lg p-[2px] transition-all duration-200 shadow-sm hover:shadow-md hover:-translate-y-0.5 bg-white dark:bg-slate-800"
|
||||
+ className={`group relative rounded-xl transition-all duration-200 bg-gray-50 dark:bg-slate-900 border ${
|
||||
+ isActive
|
||||
+ ? "border-gray-900 dark:border-white shadow-sm"
|
||||
+ : "border-transparent hover:border-gray-200 dark:hover:border-slate-700"
|
||||
+ }`}
|
||||
>
|
||||
- {/* Gradient border layer - always present, opacity controlled */}
|
||||
- <div
|
||||
- aria-hidden
|
||||
- className="absolute inset-0 rounded-lg transition-opacity duration-300 pointer-events-none"
|
||||
- style={{
|
||||
- backgroundImage: p.gradient,
|
||||
- opacity: isActive ? 1 : 0,
|
||||
- }}
|
||||
- />
|
||||
<button
|
||||
onClick={() =>
|
||||
handleProjectSelect(p.id)
|
||||
}
|
||||
- className={`relative flex flex-col w-full rounded-[6px] overflow-hidden
|
||||
- bg-white dark:bg-slate-800
|
||||
- focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo-500`}
|
||||
+ className={`relative flex flex-col w-full rounded-xl overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500`}
|
||||
aria-expanded={isActive}
|
||||
type="button"
|
||||
>
|
||||
- {/* Gradient overlay that covers the entire button; becomes visible on hover. */}
|
||||
- <div
|
||||
- aria-hidden
|
||||
- className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none rounded-[6px]"
|
||||
- style={{
|
||||
- backgroundImage:
|
||||
- p.gradient,
|
||||
- }}
|
||||
- />
|
||||
-
|
||||
- {/* Slight dark layer to insure text contrast when gradient is visible */}
|
||||
- <div
|
||||
- aria-hidden
|
||||
- className="absolute inset-0 bg-black/0 group-hover:bg-black/25 transition-colors duration-300 pointer-events-none rounded-[6px]"
|
||||
- />
|
||||
-
|
||||
{/* Image fills width */}
|
||||
- <div className="relative z-10 w-full h-24 overflow-hidden">
|
||||
+ <div className="relative z-10 w-full h-32 overflow-hidden bg-gray-200 dark:bg-slate-800">
|
||||
<ProjectThumb
|
||||
src={
|
||||
(p.repo &&
|
||||
@@ -270,11 +242,11 @@ export default function PortfolioAboveTheFold() {
|
||||
</div>
|
||||
|
||||
{/* Text below image */}
|
||||
- <div className="relative z-10 text-left p-2">
|
||||
- <div className="text-sm font-medium transition-colors duration-200 text-gray-900 dark:text-white group-hover:text-white">
|
||||
+ <div className="relative z-10 text-left p-3">
|
||||
+ <div className="text-sm font-semibold transition-colors duration-200 text-gray-900 dark:text-white">
|
||||
{p.title}
|
||||
</div>
|
||||
- <div className="text-xs transition-colors duration-200 text-gray-500 dark:text-gray-400 group-hover:text-gray-100 line-clamp-2">
|
||||
+ <div className="text-xs transition-colors duration-200 text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">
|
||||
{p.desc}
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,13 +254,59 @@ export default function PortfolioAboveTheFold() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
+ </div>
|
||||
+ </div>
|
||||
+ </div>
|
||||
+
|
||||
+ <div className="mt-8 pt-8 border-t border-gray-200/60 dark:border-slate-800/60 flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6 px-2 pb-8">
|
||||
+ <div>
|
||||
+ <h4 className="text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-widest">
|
||||
+ Skills
|
||||
+ </h4>
|
||||
+ <div className="mt-3 flex flex-wrap gap-2.5">
|
||||
+ {skills.map((s, i) => (
|
||||
+ <span
|
||||
+ key={i}
|
||||
+ className="text-[11px] font-medium tracking-wide px-3 py-1.5 bg-gray-100 dark:bg-slate-800/50 rounded-full text-gray-700 dark:text-gray-300"
|
||||
+ >
|
||||
+ {s}
|
||||
+ </span>
|
||||
+ ))}
|
||||
+ </div>
|
||||
+ </div>
|
||||
+
|
||||
+ <div className="text-left lg:text-right text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
+ <div className="flex items-center gap-2 lg:justify-end mb-1">
|
||||
+ <span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||
+ Available for freelance & contract
|
||||
+ </div>
|
||||
+ <a href={`mailto:${profile.email}`} className="hover:text-gray-900 dark:hover:text-white transition-colors">{profile.email}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
- <div className="flex flex-col gap-4 min-h-0">
|
||||
- <div className="flex-1 bg-white dark:bg-slate-800 rounded-lg border border-gray-100 dark:border-slate-700 overflow-hidden">
|
||||
+ <div
|
||||
+ className={`flex flex-col gap-6 min-h-0 h-full transition-all duration-300 fixed inset-0 z-50 p-4 sm:p-8 bg-slate-50/90 dark:bg-slate-950/90 backdrop-blur-md lg:static lg:p-0 lg:bg-transparent lg:dark:bg-transparent lg:backdrop-blur-none ${
|
||||
+ displayedProject !== null
|
||||
+ ? "opacity-100 pointer-events-auto"
|
||||
+ : "opacity-0 pointer-events-none lg:opacity-100 lg:pointer-events-auto"
|
||||
+ }`}
|
||||
+ onClick={() => {
|
||||
+ // Only close on click if it's acting as a modal (narrow viewports)
|
||||
+ if (window.innerWidth < 1024 && displayedProject !== null) {
|
||||
+ handleClose();
|
||||
+ }
|
||||
+ }}
|
||||
+ >
|
||||
+ <div
|
||||
+ className={`flex-1 bg-white dark:bg-slate-900 lg:rounded-3xl border border-gray-200/60 dark:border-slate-800/60 shadow-xl overflow-hidden rounded-2xl transition-all duration-300 ease-out transform ${
|
||||
+ displayedProject !== null
|
||||
+ ? "translate-y-0 scale-100 opacity-100"
|
||||
+ : "translate-y-8 scale-95 opacity-0 lg:translate-y-0 lg:scale-100 lg:opacity-100"
|
||||
+ }`}
|
||||
+ onClick={(e) => e.stopPropagation()}
|
||||
+ >
|
||||
<div className="w-full h-full flex items-center justify-center relative">
|
||||
{/* Project detail view */}
|
||||
<FadeContainer
|
||||
@@ -302,12 +320,22 @@ export default function PortfolioAboveTheFold() {
|
||||
className="w-full h-full"
|
||||
scrollbarOffset={12}
|
||||
>
|
||||
- <article>
|
||||
+ <article className="p-6 md:p-10">
|
||||
+ <button
|
||||
+ onClick={handleClose}
|
||||
+ className="absolute z-50 top-4 right-4 md:top-6 md:right-6 p-2 rounded-full bg-white/80 dark:bg-slate-900/80 backdrop-blur text-gray-500 hover:text-gray-900 hover:bg-gray-100 dark:hover:text-white dark:hover:bg-slate-800 border border-gray-200/50 dark:border-slate-700/50 shadow-sm transition-all lg:hidden"
|
||||
+ aria-label="Close project"
|
||||
+ >
|
||||
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
+ <line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
+ <line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
+ </svg>
|
||||
+ </button>
|
||||
<CascadeItem
|
||||
delay={0}
|
||||
isVisible={isContentVisible}
|
||||
>
|
||||
- <div className="w-full aspect-[4/3] rounded-lg overflow-hidden bg-gray-100 dark:bg-slate-700">
|
||||
+ <div className="w-full aspect-video rounded-xl overflow-hidden bg-gray-100 dark:bg-slate-800 border border-gray-200/50 dark:border-slate-700/50">
|
||||
<ProjectThumb
|
||||
src={
|
||||
readmeImage ||
|
||||
@@ -333,8 +361,8 @@ export default function PortfolioAboveTheFold() {
|
||||
delay={75}
|
||||
isVisible={isContentVisible}
|
||||
>
|
||||
- <div className="mt-4 flex items-start justify-between">
|
||||
- <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
+ <div className="mt-6 flex items-start justify-between">
|
||||
+ <h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
{
|
||||
allProjectsList.find(
|
||||
(p) =>
|
||||
@@ -345,9 +373,13 @@ export default function PortfolioAboveTheFold() {
|
||||
</h2>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
- className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
||||
+ className="hidden lg:block p-2 -mr-2 rounded-full text-gray-400 hover:text-gray-900 hover:bg-gray-100 dark:hover:text-white dark:hover:bg-slate-800 transition-colors"
|
||||
+ aria-label="Close project"
|
||||
>
|
||||
- Close
|
||||
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
+ <line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
+ <line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
+ </svg>
|
||||
</button>
|
||||
</div>
|
||||
</CascadeItem>
|
||||
@@ -366,7 +398,7 @@ export default function PortfolioAboveTheFold() {
|
||||
?.tags.map((t, i) => (
|
||||
<span
|
||||
key={i}
|
||||
- className="text-xs px-2 py-1 bg-gray-100 dark:bg-slate-700 rounded-md text-gray-700 dark:text-gray-300"
|
||||
+ className="text-[11px] font-medium tracking-wide uppercase px-2.5 py-1 bg-white dark:bg-slate-800 border border-gray-200 dark:border-slate-700 rounded-full text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
@@ -380,13 +412,15 @@ export default function PortfolioAboveTheFold() {
|
||||
>
|
||||
<div className="mt-4">
|
||||
{readmeLoading && (
|
||||
- <div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
- Loading README...
|
||||
+ <div className="flex flex-col gap-3 mt-8 opacity-50 animate-pulse w-full max-w-lg">
|
||||
+ <div className="h-4 bg-gray-200 dark:bg-slate-800 rounded-full w-3/4"></div>
|
||||
+ <div className="h-4 bg-gray-200 dark:bg-slate-800 rounded-full w-1/2"></div>
|
||||
+ <div className="h-4 bg-gray-200 dark:bg-slate-800 rounded-full w-5/6"></div>
|
||||
</div>
|
||||
)}
|
||||
{readmeError &&
|
||||
!readmeLoading && (
|
||||
- <p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
+ <p className="text-base text-gray-600 dark:text-gray-400 mt-6 leading-relaxed">
|
||||
{
|
||||
allProjectsList.find(
|
||||
(p) =>
|
||||
@@ -398,7 +432,7 @@ export default function PortfolioAboveTheFold() {
|
||||
)}
|
||||
{readmeContent &&
|
||||
!readmeLoading && (
|
||||
- <div className="prose prose-sm dark:prose-invert max-w-none text-gray-700 dark:text-gray-300">
|
||||
+ <div className="prose prose-base md:prose-lg dark:prose-invert max-w-none text-gray-600 dark:text-gray-300 mt-8 prose-headings:font-semibold prose-a:text-blue-600 dark:prose-a:text-blue-400">
|
||||
<Markdown
|
||||
remarkPlugins={[
|
||||
remarkGfm,
|
||||
@@ -468,7 +502,7 @@ export default function PortfolioAboveTheFold() {
|
||||
)}
|
||||
{!activeRepo &&
|
||||
!readmeLoading && (
|
||||
- <p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
+ <p className="text-base text-gray-600 dark:text-gray-400 leading-relaxed mt-6">
|
||||
{
|
||||
allProjectsList.find(
|
||||
(p) =>
|
||||
@@ -487,12 +521,13 @@ export default function PortfolioAboveTheFold() {
|
||||
|
||||
{/* Empty state view */}
|
||||
<FadeContainer
|
||||
+ className="hidden lg:flex"
|
||||
isVisible={
|
||||
displayedProject === null &&
|
||||
isContentVisible
|
||||
}
|
||||
>
|
||||
- <div className="text-center max-w-[44ch] flex flex-col items-center justify-center h-full">
|
||||
+ <div className="text-center max-w-[44ch] flex flex-col items-center justify-center h-full p-6 md:p-10">
|
||||
<CascadeItem
|
||||
delay={0}
|
||||
isVisible={
|
||||
@@ -500,8 +535,8 @@ export default function PortfolioAboveTheFold() {
|
||||
isContentVisible
|
||||
}
|
||||
>
|
||||
- <h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
- Selected work
|
||||
+ <h2 className="text-2xl font-semibold tracking-tight text-gray-900 dark:text-white">
|
||||
+ Selected Work
|
||||
</h2>
|
||||
</CascadeItem>
|
||||
|
||||
@@ -512,9 +547,9 @@ export default function PortfolioAboveTheFold() {
|
||||
isContentVisible
|
||||
}
|
||||
>
|
||||
- <p className="mt-3 text-sm text-gray-600 dark:text-gray-400">
|
||||
+ <p className="mt-4 text-base text-gray-500 dark:text-gray-400 max-w-sm mx-auto">
|
||||
Click a project on the left to open
|
||||
- a short preview.
|
||||
+ a quick preview and read more.
|
||||
</p>
|
||||
</CascadeItem>
|
||||
|
||||
@@ -531,29 +566,6 @@ export default function PortfolioAboveTheFold() {
|
||||
</FadeContainer>
|
||||
</div>
|
||||
</div>
|
||||
-
|
||||
- <div className="flex items-center justify-between gap-3">
|
||||
- <div>
|
||||
- <h4 className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||
- Skills
|
||||
- </h4>
|
||||
- <div className="mt-2 flex flex-wrap gap-2">
|
||||
- {skills.map((s, i) => (
|
||||
- <span
|
||||
- key={i}
|
||||
- className="text-xs px-2 py-1 border border-gray-200 dark:border-slate-600 rounded-md text-gray-700 dark:text-gray-300"
|
||||
- >
|
||||
- {s}
|
||||
- </span>
|
||||
- ))}
|
||||
- </div>
|
||||
- </div>
|
||||
-
|
||||
- <div className="text-right text-xs text-gray-500 dark:text-gray-400">
|
||||
- <div>Available for freelance & contract</div>
|
||||
- <div className="mt-2">{profile.email}</div>
|
||||
- </div>
|
||||
- </div>
|
||||
</div>
|
||||
|
||||
<div className="pointer-events-none absolute bottom-6 right-6 text-[10px] text-gray-400 dark:text-gray-500">
|
||||
+142
-131
@@ -5,7 +5,6 @@ import remarkGfm from "remark-gfm";
|
||||
|
||||
// Components
|
||||
import {
|
||||
AnimatedBackground,
|
||||
ProjectThumb,
|
||||
CascadeItem,
|
||||
FadeContainer,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
|
||||
// Hooks
|
||||
import {
|
||||
useSystemDarkMode,
|
||||
useGitHubReadme,
|
||||
useGitHubRepoImages,
|
||||
} from "./hooks";
|
||||
@@ -31,13 +29,27 @@ export default function PortfolioAboveTheFold() {
|
||||
null,
|
||||
);
|
||||
const [isContentVisible, setIsContentVisible] = useState(true);
|
||||
const isDark = useSystemDarkMode();
|
||||
|
||||
// Handle project selection with fade transition
|
||||
const handleProjectSelect = (projectId: number) => {
|
||||
if (projectId === active) return;
|
||||
|
||||
// Start fade out
|
||||
// If on mobile/tablet, trigger the modal instantly without waiting for fade-out
|
||||
if (window.innerWidth < 1024) {
|
||||
setIsContentVisible(false); // Reset content visibility for the cascade
|
||||
setActive(projectId);
|
||||
setDisplayedProject(projectId);
|
||||
|
||||
// Wait for the modal wrapper to begin its CSS transition before starting the cascade
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setIsContentVisible(true);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Start fade out for desktop
|
||||
setIsContentVisible(false);
|
||||
|
||||
// After fade out completes, update the project and fade in
|
||||
@@ -48,19 +60,31 @@ export default function PortfolioAboveTheFold() {
|
||||
requestAnimationFrame(() => {
|
||||
setIsContentVisible(true);
|
||||
});
|
||||
}, 200); // Match the fade-out duration
|
||||
}, 300); // Match the fade-out duration
|
||||
};
|
||||
|
||||
// Handle closing with fade transition
|
||||
const handleClose = () => {
|
||||
setIsContentVisible(false);
|
||||
|
||||
if (window.innerWidth < 1024) {
|
||||
// Instantly start scaling down the modal on mobile
|
||||
setActive(null);
|
||||
setDisplayedProject(null);
|
||||
requestAnimationFrame(() => {
|
||||
setIsContentVisible(true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// On desktop, wait for fade-out before resetting project
|
||||
setTimeout(() => {
|
||||
setActive(null);
|
||||
setDisplayedProject(null);
|
||||
requestAnimationFrame(() => {
|
||||
setIsContentVisible(true);
|
||||
});
|
||||
}, 200);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// Combine all projects for lookup
|
||||
@@ -87,40 +111,36 @@ export default function PortfolioAboveTheFold() {
|
||||
} = useGitHubReadme(activeRepo);
|
||||
|
||||
return (
|
||||
<main className="h-screen w-full flex items-center justify-center p-6 overflow-hidden relative">
|
||||
<AnimatedBackground
|
||||
palette={activeProject?.palette}
|
||||
isDark={isDark}
|
||||
/>
|
||||
<section className="relative max-w-[1100px] w-full h-[88vh] bg-white/60 dark:bg-slate-900/60 backdrop-blur-sm border border-white/60 dark:border-slate-700/60 rounded-2xl shadow-2xl p-6 grid grid-cols-2 gap-6 items-stretch">
|
||||
<ScrollArea className="min-h-0" scrollbarOffset={12}>
|
||||
<main className="h-screen w-full bg-white dark:bg-slate-950 overflow-hidden font-sans text-slate-900 dark:text-slate-100 transition-colors duration-300">
|
||||
<section className="mx-auto max-w-[1400px] w-full h-full grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-stretch px-6 py-6 md:px-12 md:py-12 lg:px-16 lg:py-16 relative">
|
||||
<ScrollArea className="min-h-0 pr-4 lg:pr-6" scrollbarOffset={12}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
<p className="text-sm font-medium tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Hello, I'm
|
||||
</p>
|
||||
<h1 className="mt-2 text-[clamp(22px,3.6vw,40px)] leading-tight font-semibold text-gray-900 dark:text-white">
|
||||
<h1 className="mt-2 text-[clamp(32px,5vw,56px)] leading-none font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
{profile.name}
|
||||
</h1>
|
||||
<p className="mt-1 text-[clamp(14px,1.6vw,18px)] text-gray-600 dark:text-gray-300">
|
||||
{profile.role} • {profile.location}
|
||||
<p className="mt-2 text-[clamp(16px,2vw,22px)] font-medium text-gray-600 dark:text-gray-300">
|
||||
{profile.role} <span className="text-gray-300 dark:text-gray-600 mx-2">•</span> {profile.location}
|
||||
</p>
|
||||
|
||||
<p className="mt-6 text-[clamp(13px,1.2vw,16px)] text-gray-700 dark:text-gray-300 max-w-[38ch]">
|
||||
<p className="mt-8 text-base lg:text-lg text-gray-600 dark:text-gray-400 max-w-[45ch] leading-relaxed">
|
||||
{profile.blurb}
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<div className="mt-8 flex flex-wrap gap-4">
|
||||
<a
|
||||
href={profile.cv}
|
||||
className="inline-flex items-center gap-2 rounded-md px-4 py-2 border border-gray-200 dark:border-slate-600 shadow-sm text-sm font-medium text-gray-900 dark:text-white hover:shadow hover:-translate-y-0.5 transition-transform"
|
||||
className="inline-flex items-center gap-2 rounded-full px-6 py-2.5 border border-gray-200 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm text-sm font-medium text-gray-900 dark:text-white hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
Download CV
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={`mailto:${profile.email}`}
|
||||
className="inline-flex items-center gap-2 rounded-md px-4 py-2 bg-gray-900 dark:bg-white text-white dark:text-gray-900 text-sm font-medium hover:opacity-95 transition-opacity"
|
||||
className="inline-flex items-center gap-2 rounded-full px-6 py-2.5 bg-gray-900 dark:bg-white text-white dark:text-gray-900 text-sm font-medium hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
Contact
|
||||
</a>
|
||||
@@ -137,48 +157,24 @@ export default function PortfolioAboveTheFold() {
|
||||
const isActive = active === p.id;
|
||||
|
||||
return (
|
||||
// Wrapper div creates the gradient border effect
|
||||
<div
|
||||
key={p.id}
|
||||
className="group relative rounded-lg p-[2px] transition-all duration-200 shadow-sm hover:shadow-md hover:-translate-y-0.5 bg-white dark:bg-slate-800"
|
||||
className={`group relative rounded-xl transition-all duration-200 bg-gray-50 dark:bg-slate-900 border ${
|
||||
isActive
|
||||
? "border-gray-900 dark:border-white shadow-sm"
|
||||
: "border-transparent hover:border-gray-200 dark:hover:border-slate-700"
|
||||
}`}
|
||||
>
|
||||
{/* Gradient border layer - always present, opacity controlled */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 rounded-lg transition-opacity duration-300 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: p.gradient,
|
||||
opacity: isActive ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleProjectSelect(p.id)
|
||||
}
|
||||
className={`relative flex flex-col w-full rounded-[6px] overflow-hidden
|
||||
bg-white dark:bg-slate-800
|
||||
focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo-500`}
|
||||
className={`relative flex flex-col w-full rounded-xl overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500`}
|
||||
aria-expanded={isActive}
|
||||
type="button"
|
||||
>
|
||||
{/* Gradient overlay that covers the entire button; becomes visible on hover. */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none rounded-[6px]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
p.gradient,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Slight dark layer to insure text contrast when gradient is visible */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 bg-black/0 group-hover:bg-black/25 transition-colors duration-300 pointer-events-none rounded-[6px]"
|
||||
/>
|
||||
|
||||
{/* Image fills width */}
|
||||
<div className="relative z-10 w-full h-24 overflow-hidden">
|
||||
<div className="relative z-10 w-full h-32 overflow-hidden bg-gray-200 dark:bg-slate-800">
|
||||
<ProjectThumb
|
||||
src={
|
||||
(p.repo &&
|
||||
@@ -192,11 +188,11 @@ export default function PortfolioAboveTheFold() {
|
||||
</div>
|
||||
|
||||
{/* Text below image */}
|
||||
<div className="relative z-10 text-left p-2">
|
||||
<div className="text-sm font-medium transition-colors duration-200 text-gray-900 dark:text-white group-hover:text-white">
|
||||
<div className="relative z-10 text-left p-3">
|
||||
<div className="text-sm font-semibold transition-colors duration-200 text-gray-900 dark:text-white">
|
||||
{p.title}
|
||||
</div>
|
||||
<div className="text-xs transition-colors duration-200 text-gray-500 dark:text-gray-400 group-hover:text-gray-100 line-clamp-2">
|
||||
<div className="text-xs transition-colors duration-200 text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">
|
||||
{p.desc}
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,48 +211,24 @@ export default function PortfolioAboveTheFold() {
|
||||
const isActive = active === p.id;
|
||||
|
||||
return (
|
||||
// Wrapper div creates the gradient border effect
|
||||
<div
|
||||
key={p.id}
|
||||
className="group relative rounded-lg p-[2px] transition-all duration-200 shadow-sm hover:shadow-md hover:-translate-y-0.5 bg-white dark:bg-slate-800"
|
||||
className={`group relative rounded-xl transition-all duration-200 bg-gray-50 dark:bg-slate-900 border ${
|
||||
isActive
|
||||
? "border-gray-900 dark:border-white shadow-sm"
|
||||
: "border-transparent hover:border-gray-200 dark:hover:border-slate-700"
|
||||
}`}
|
||||
>
|
||||
{/* Gradient border layer - always present, opacity controlled */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 rounded-lg transition-opacity duration-300 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: p.gradient,
|
||||
opacity: isActive ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleProjectSelect(p.id)
|
||||
}
|
||||
className={`relative flex flex-col w-full rounded-[6px] overflow-hidden
|
||||
bg-white dark:bg-slate-800
|
||||
focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo-500`}
|
||||
className={`relative flex flex-col w-full rounded-xl overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500`}
|
||||
aria-expanded={isActive}
|
||||
type="button"
|
||||
>
|
||||
{/* Gradient overlay that covers the entire button; becomes visible on hover. */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none rounded-[6px]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
p.gradient,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Slight dark layer to insure text contrast when gradient is visible */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 bg-black/0 group-hover:bg-black/25 transition-colors duration-300 pointer-events-none rounded-[6px]"
|
||||
/>
|
||||
|
||||
{/* Image fills width */}
|
||||
<div className="relative z-10 w-full h-24 overflow-hidden">
|
||||
<div className="relative z-10 w-full h-32 overflow-hidden bg-gray-200 dark:bg-slate-800">
|
||||
<ProjectThumb
|
||||
src={
|
||||
(p.repo &&
|
||||
@@ -270,11 +242,11 @@ export default function PortfolioAboveTheFold() {
|
||||
</div>
|
||||
|
||||
{/* Text below image */}
|
||||
<div className="relative z-10 text-left p-2">
|
||||
<div className="text-sm font-medium transition-colors duration-200 text-gray-900 dark:text-white group-hover:text-white">
|
||||
<div className="relative z-10 text-left p-3">
|
||||
<div className="text-sm font-semibold transition-colors duration-200 text-gray-900 dark:text-white">
|
||||
{p.title}
|
||||
</div>
|
||||
<div className="text-xs transition-colors duration-200 text-gray-500 dark:text-gray-400 group-hover:text-gray-100 line-clamp-2">
|
||||
<div className="text-xs transition-colors duration-200 text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">
|
||||
{p.desc}
|
||||
</div>
|
||||
</div>
|
||||
@@ -285,10 +257,55 @@ export default function PortfolioAboveTheFold() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-gray-200/60 dark:border-slate-800/60 flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6 px-2 pb-8">
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-widest">
|
||||
Skills
|
||||
</h4>
|
||||
<div className="mt-3 flex flex-wrap gap-2.5">
|
||||
{skills.map((s, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="text-[11px] font-medium tracking-wide px-3 py-1.5 bg-gray-100 dark:bg-slate-800/50 rounded-full text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-left lg:text-right text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<div className="flex items-center gap-2 lg:justify-end mb-1">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||
Available for freelance & contract
|
||||
</div>
|
||||
<a href={`mailto:${profile.email}`} className="hover:text-gray-900 dark:hover:text-white transition-colors">{profile.email}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="flex flex-col gap-4 min-h-0">
|
||||
<div className="flex-1 bg-white dark:bg-slate-800 rounded-lg border border-gray-100 dark:border-slate-700 overflow-hidden">
|
||||
<div
|
||||
className={`flex flex-col gap-6 min-h-0 h-full transition-all duration-300 fixed inset-0 z-50 p-4 sm:p-8 bg-slate-50/90 dark:bg-slate-950/90 backdrop-blur-md lg:static lg:p-0 lg:bg-transparent lg:dark:bg-transparent lg:backdrop-blur-none ${
|
||||
displayedProject !== null
|
||||
? "opacity-100 pointer-events-auto"
|
||||
: "opacity-0 pointer-events-none lg:opacity-100 lg:pointer-events-auto"
|
||||
}`}
|
||||
onClick={() => {
|
||||
// Only close on click if it's acting as a modal (narrow viewports)
|
||||
if (window.innerWidth < 1024 && displayedProject !== null) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`flex-1 bg-white dark:bg-slate-900 lg:rounded-3xl border border-gray-200/60 dark:border-slate-800/60 shadow-xl overflow-hidden rounded-2xl transition-all duration-300 ease-out transform ${
|
||||
displayedProject !== null
|
||||
? "translate-y-0 scale-100 opacity-100"
|
||||
: "translate-y-8 scale-95 opacity-0 lg:translate-y-0 lg:scale-100 lg:opacity-100"
|
||||
}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="w-full h-full flex items-center justify-center relative">
|
||||
{/* Project detail view */}
|
||||
<FadeContainer
|
||||
@@ -302,12 +319,22 @@ export default function PortfolioAboveTheFold() {
|
||||
className="w-full h-full"
|
||||
scrollbarOffset={12}
|
||||
>
|
||||
<article>
|
||||
<article className="p-6 md:p-10">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="absolute z-50 top-4 right-4 md:top-6 md:right-6 p-2 rounded-full bg-white/80 dark:bg-slate-900/80 backdrop-blur text-gray-500 hover:text-gray-900 hover:bg-gray-100 dark:hover:text-white dark:hover:bg-slate-800 border border-gray-200/50 dark:border-slate-700/50 shadow-sm transition-all lg:hidden"
|
||||
aria-label="Close project"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<CascadeItem
|
||||
delay={0}
|
||||
isVisible={isContentVisible}
|
||||
>
|
||||
<div className="w-full aspect-[4/3] rounded-lg overflow-hidden bg-gray-100 dark:bg-slate-700">
|
||||
<div className="w-full aspect-video rounded-xl overflow-hidden bg-gray-100 dark:bg-slate-800 border border-gray-200/50 dark:border-slate-700/50">
|
||||
<ProjectThumb
|
||||
src={
|
||||
readmeImage ||
|
||||
@@ -333,8 +360,8 @@ export default function PortfolioAboveTheFold() {
|
||||
delay={75}
|
||||
isVisible={isContentVisible}
|
||||
>
|
||||
<div className="mt-4 flex items-start justify-between">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
<div className="mt-6 flex items-start justify-between">
|
||||
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
{
|
||||
allProjectsList.find(
|
||||
(p) =>
|
||||
@@ -345,9 +372,13 @@ export default function PortfolioAboveTheFold() {
|
||||
</h2>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
||||
className="hidden lg:block p-2 -mr-2 rounded-full text-gray-400 hover:text-gray-900 hover:bg-gray-100 dark:hover:text-white dark:hover:bg-slate-800 transition-colors"
|
||||
aria-label="Close project"
|
||||
>
|
||||
Close
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</CascadeItem>
|
||||
@@ -366,7 +397,7 @@ export default function PortfolioAboveTheFold() {
|
||||
?.tags.map((t, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="text-xs px-2 py-1 bg-gray-100 dark:bg-slate-700 rounded-md text-gray-700 dark:text-gray-300"
|
||||
className="text-[11px] font-medium tracking-wide uppercase px-2.5 py-1 bg-white dark:bg-slate-800 border border-gray-200 dark:border-slate-700 rounded-full text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
@@ -380,13 +411,15 @@ export default function PortfolioAboveTheFold() {
|
||||
>
|
||||
<div className="mt-4">
|
||||
{readmeLoading && (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Loading README...
|
||||
<div className="flex flex-col gap-3 mt-8 opacity-50 animate-pulse w-full max-w-lg">
|
||||
<div className="h-4 bg-gray-200 dark:bg-slate-800 rounded-full w-3/4"></div>
|
||||
<div className="h-4 bg-gray-200 dark:bg-slate-800 rounded-full w-1/2"></div>
|
||||
<div className="h-4 bg-gray-200 dark:bg-slate-800 rounded-full w-5/6"></div>
|
||||
</div>
|
||||
)}
|
||||
{readmeError &&
|
||||
!readmeLoading && (
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
<p className="text-base text-gray-600 dark:text-gray-400 mt-6 leading-relaxed">
|
||||
{
|
||||
allProjectsList.find(
|
||||
(p) =>
|
||||
@@ -398,7 +431,7 @@ export default function PortfolioAboveTheFold() {
|
||||
)}
|
||||
{readmeContent &&
|
||||
!readmeLoading && (
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-gray-700 dark:text-gray-300">
|
||||
<div className="prose prose-base md:prose-lg dark:prose-invert max-w-none text-gray-600 dark:text-gray-300 mt-8 prose-headings:font-semibold prose-a:text-blue-600 dark:prose-a:text-blue-400">
|
||||
<Markdown
|
||||
remarkPlugins={[
|
||||
remarkGfm,
|
||||
@@ -468,7 +501,7 @@ export default function PortfolioAboveTheFold() {
|
||||
)}
|
||||
{!activeRepo &&
|
||||
!readmeLoading && (
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
<p className="text-base text-gray-600 dark:text-gray-400 leading-relaxed mt-6">
|
||||
{
|
||||
allProjectsList.find(
|
||||
(p) =>
|
||||
@@ -487,12 +520,13 @@ export default function PortfolioAboveTheFold() {
|
||||
|
||||
{/* Empty state view */}
|
||||
<FadeContainer
|
||||
className="hidden lg:flex"
|
||||
isVisible={
|
||||
displayedProject === null &&
|
||||
isContentVisible
|
||||
}
|
||||
>
|
||||
<div className="text-center max-w-[44ch] flex flex-col items-center justify-center h-full">
|
||||
<div className="text-center max-w-[44ch] flex flex-col items-center justify-center h-full p-6 md:p-10">
|
||||
<CascadeItem
|
||||
delay={0}
|
||||
isVisible={
|
||||
@@ -500,8 +534,8 @@ export default function PortfolioAboveTheFold() {
|
||||
isContentVisible
|
||||
}
|
||||
>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Selected work
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-gray-900 dark:text-white">
|
||||
Selected Work
|
||||
</h2>
|
||||
</CascadeItem>
|
||||
|
||||
@@ -512,9 +546,9 @@ export default function PortfolioAboveTheFold() {
|
||||
isContentVisible
|
||||
}
|
||||
>
|
||||
<p className="mt-3 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p className="mt-4 text-base text-gray-500 dark:text-gray-400 max-w-sm mx-auto">
|
||||
Click a project on the left to open
|
||||
a short preview.
|
||||
a quick preview and read more.
|
||||
</p>
|
||||
</CascadeItem>
|
||||
|
||||
@@ -531,29 +565,6 @@ export default function PortfolioAboveTheFold() {
|
||||
</FadeContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h4 className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||
Skills
|
||||
</h4>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{skills.map((s, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="text-xs px-2 py-1 border border-gray-200 dark:border-slate-600 rounded-md text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right text-xs text-gray-500 dark:text-gray-400">
|
||||
<div>Available for freelance & contract</div>
|
||||
<div className="mt-2">{profile.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pointer-events-none absolute bottom-6 right-6 text-[10px] text-gray-400 dark:text-gray-500">
|
||||
|
||||
@@ -3,16 +3,18 @@ import React from "react";
|
||||
interface FadeContainerProps {
|
||||
children: React.ReactNode;
|
||||
isVisible: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// FadeContainer: fades content in/out using CSS transitions only
|
||||
export default function FadeContainer({
|
||||
children,
|
||||
isVisible,
|
||||
className = "",
|
||||
}: FadeContainerProps) {
|
||||
return (
|
||||
<div
|
||||
className="transition-opacity duration-200 ease-out w-full h-full absolute inset-0 flex items-center justify-center p-6"
|
||||
className={`transition-opacity duration-200 ease-out w-full h-full absolute inset-0 ${className}`}
|
||||
style={{
|
||||
opacity: isVisible ? 1 : 0,
|
||||
pointerEvents: isVisible ? "auto" : "none",
|
||||
|
||||
Reference in New Issue
Block a user