- Added dynamic scroll prompt functionality triggered by timeline completion.

- Introduced reusable `Nav` and `Footer` components.
- Updated layout with new navigation, footer, and responsive styling.
- Refactored timeline buttons for clarity and relocation.
- Improved global and scroll prompt styles for better UI/UX.
- Set up mobile responsiveness for navigation bar.
This commit is contained in:
IM23a-spirgif
2025-05-22 10:37:36 +02:00
parent 96a9e0b733
commit 777f783ad5
5 changed files with 214 additions and 143 deletions
+72 -98
View File
@@ -72,9 +72,11 @@ export default function About() {
const sectionRefs = useRef([]); const sectionRefs = useRef([]);
const aboutMeRef = useRef(null); const aboutMeRef = useRef(null);
const buttonSectionRef = useRef(null);
const scrollContainerRef = useRef(null); const scrollContainerRef = useRef(null);
const [currentSection, setCurrentSection] = useState(0); const [currentSection, setCurrentSection] = useState(0);
const [timelineFinished, setTimelineFinished] = useState(false); const [timelineFinished, setTimelineFinished] = useState(false);
const [showScrollPrompt, setShowScrollPrompt] = useState(false);
useEffect(() => { useEffect(() => {
setCurrentSection(0); setCurrentSection(0);
setTimelineFinished(false); setTimelineFinished(false);
@@ -82,6 +84,26 @@ export default function About() {
aboutMeRef.current?.scrollIntoView({behavior: "smooth", block: "start"}); aboutMeRef.current?.scrollIntoView({behavior: "smooth", block: "start"});
}, 100); }, 100);
}, []); }, []);
useEffect(() => {
let scrollListener;
if (timelineFinished) {
setShowScrollPrompt(true);
scrollListener = () => {
setShowScrollPrompt(false);
window.removeEventListener("scroll", scrollListener);
};
window.addEventListener("scroll", scrollListener);
}
return () => {
if (scrollListener) {
window.removeEventListener("scroll", scrollListener);
}
};
}, [timelineFinished]);
const handleButtonClick = (direction) => { const handleButtonClick = (direction) => {
setCurrentSection((prev) => { setCurrentSection((prev) => {
const newSection = Math.max(0, Math.min(prev + direction, timelineData.length - 1)); const newSection = Math.max(0, Math.min(prev + direction, timelineData.length - 1));
@@ -115,7 +137,12 @@ export default function About() {
return () => observer.disconnect(); return () => observer.disconnect();
}, [timelineData.length]); }, [timelineData.length]);
return ( return (
<div style={{width: "100%", overflowX: "hidden"}}> // Ändern Sie den äußeren Container-Style
<div style={{
width: "100%",
position: "relative", // Hinzugefügt
overflowX: "hidden"
}}>
{/* About Me Section */} {/* About Me Section */}
<section <section
ref={aboutMeRef} ref={aboutMeRef}
@@ -136,60 +163,9 @@ export default function About() {
</p> </p>
</section> </section>
{/* Timeline Section */} {/* Timeline Section */}
<section style={{position: "relative", height: "100vh", overflowY: "hidden"}}> <section style={{ position: "relative", height: "100vh", overflowY: "hidden" }}>
{currentSection > 0 && ( {/* Navigation buttons and timeline content here */}
<button <div ref={scrollContainerRef} style={{ display: "flex", overflowX: "auto", scrollSnapType: "x mandatory", width: "100%", height: "100%", scrollBehavior: "smooth", scrollbarWidth: "none", msOverflowStyle: "none" }}>
onClick={() => handleButtonClick(-1)}
style={{
position: "absolute",
top: "50%",
left: "1rem",
transform: "translateY(-50%)",
zIndex: 10,
backgroundColor: "#333",
color: "#fff",
border: "none",
padding: "0.75rem 1rem",
cursor: "pointer",
borderRadius: "4px",
}}
>
&larr;
</button>
)}
{currentSection < timelineData.length - 1 && (
<button
onClick={() => handleButtonClick(1)}
style={{
position: "absolute",
top: "50%",
right: "1rem",
transform: "translateY(-50%)",
zIndex: 10,
backgroundColor: "#333",
color: "#fff",
border: "none",
padding: "0.75rem 1rem",
cursor: "pointer",
borderRadius: "4px",
}}
>
&rarr;
</button>
)}
<div
ref={scrollContainerRef}
style={{
display: "flex",
overflowX: "auto",
scrollSnapType: "x mandatory",
width: "100%",
height: "100%",
scrollBehavior: "smooth",
scrollbarWidth: "none",
msOverflowStyle: "none",
}}
>
{timelineData.map((item, index) => { {timelineData.map((item, index) => {
const adjustedSkills = skills.map((s) => { const adjustedSkills = skills.map((s) => {
const stepLevel = s.progression?.[index] ?? 0; const stepLevel = s.progression?.[index] ?? 0;
@@ -213,61 +189,61 @@ export default function About() {
boxSizing: "border-box", boxSizing: "border-box",
}} }}
> >
<h2 style={{marginBottom: "1rem"}}>{item.title}</h2> <h2>{item.title}</h2>
<h3 style={{marginBottom: "1rem"}}>{item.date}</h3> <h3>{item.date}</h3>
<p style={{maxWidth: "600px", textAlign: "center", marginBottom: "2rem"}}> <p>{item.content}</p>
{item.content} <div style={{ display: "flex", gap: "2rem", flexWrap: "wrap", justifyContent: "center" }}>
</p>
{/* SKILLS: show per stage */}
<div style={{display: "flex", gap: "2rem"}}>
<div style={{
display: "flex",
gap: "2rem",
flexWrap: "wrap",
justifyContent: "center"
}}>
{adjustedSkills.filter((s) => s.level > 0).map((skill, sIndex) => ( {adjustedSkills.filter((s) => s.level > 0).map((skill, sIndex) => (
<div <div
key={`${skill.name}-${index}`} key={`${skill.name}-${index}`}
className={currentSection === index ? "skill-card animate-skill" : "skill-card"} className={currentSection === index ? "skill-card animate-skill" : "skill-card"}
style={{ style={{ textAlign: "center", width: "120px", animationDelay: `${sIndex * 100}ms`, animationFillMode: "both" }}
textAlign: "center",
width: "120px",
animationDelay: `${sIndex * 100}ms`,
animationFillMode: "both",
}}
>
<p style={{
fontSize: "1.1rem",
fontWeight: "bold",
marginBottom: "0.5rem"
}}>
{skill.name}
</p>
<div
style={{
padding: "0.5rem 1rem",
backgroundColor: "#1a001a",
border: "2px solid #3d003d",
borderRadius: "10px",
fontSize: "1rem",
color: "#fff",
}}
> >
<p style={{ fontWeight: "bold" }}>{skill.name}</p>
<div style={{ padding: "0.5rem 1rem", backgroundColor: "#1a001a", border: "2px solid #3d003d", borderRadius: "10px", color: "#fff" }}>
{getSkillLabel(skill.level)} {getSkillLabel(skill.level)}
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div>
</section> </section>
); );
})} })}
</div> </div>
{/* End of scroll container */}
{currentSection > 0 && (
<button onClick={() => handleButtonClick(-1)} style={{ position: "absolute", top: "50%", left: "1rem", transform: "translateY(-50%)", zIndex: 10, backgroundColor: "#333", color: "#fff", border: "none", padding: "0.75rem 1rem", cursor: "pointer", borderRadius: "4px" }}>
&larr;
</button>
)}
{currentSection < timelineData.length - 1 && (
<button onClick={() => handleButtonClick(1)} style={{ position: "absolute", top: "50%", right: "1rem", transform: "translateY(-50%)", zIndex: 10, backgroundColor: "#333", color: "#fff", border: "none", padding: "0.75rem 1rem", cursor: "pointer", borderRadius: "4px" }}>
&rarr;
</button>
)}
</section>
{timelineFinished && showScrollPrompt && (
<div className="scrollPrompt">
⬇ Scroll down to continue ⬇
</div>
)}
{/* FINAL BUTTON SECTION - MOVED OUTSIDE */}
{timelineFinished && ( {timelineFinished && (
<div style={{marginTop: "3rem", textAlign: "center", width: "100%"}}> // Fügen Sie dem Button-Section einen z-Index hinzu
<section
ref={buttonSectionRef}
style={{
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
padding: "2rem",
boxSizing: "border-box",
position: "relative", // Hinzugefügt
zIndex: 1 // Hinzugefügt
}}
>
<Link href="/projects"> <Link href="/projects">
<button <button
className="learnMoreButton" className="learnMoreButton"
@@ -287,10 +263,8 @@ export default function About() {
View Projects View Projects
</button> </button>
</Link> </Link>
</div>
)}
</section> </section>
)}
</div> </div>
) )
} }
+15
View File
@@ -0,0 +1,15 @@
export default function Footer() {
return (
<footer style={{
padding: "2rem",
textAlign: "center",
background: "#1a001a",
borderTop: "2px solid #3d003d",
marginTop: "auto"
}}>
<p style={{ margin: 0, color: "#ccc" }}>
© {new Date().getFullYear()} Frederik Spirgi. All rights reserved.
</p>
</footer>
);
}
+15
View File
@@ -0,0 +1,15 @@
"use client";
import Link from "next/link";
export default function Nav() {
return (
<nav className="navbar">
<Link href="/" className="nav-logo">Fretux</Link>
<div className="nav-links">
<Link href="/about" className="nav-link">About</Link>
<Link href="/projects" className="nav-link">Projects</Link>
<Link href="/contact" className="nav-link">Contact</Link>
</div>
</nav>
);
}
+63 -6
View File
@@ -138,16 +138,18 @@ html {
.scrollPrompt { .scrollPrompt {
position: fixed; position: fixed;
bottom: 20px; bottom: 40px;
left: 50%; left: 0;
transform: translateX(-50%); width: 100%;
padding: 0.5rem 1rem; text-align: center;
border-radius: 5px;
color: #fff;
font-size: 1rem; font-size: 1rem;
padding: 0.5rem 1rem;
color: #fff;
border-radius: 5px;
z-index: 1000; z-index: 1000;
background: none; background: none;
animation: pulsePrompt 2s infinite ease-in-out; animation: pulsePrompt 2s infinite ease-in-out;
pointer-events: none;
} }
.fade-in-section.is-visible { .fade-in-section.is-visible {
@@ -312,3 +314,58 @@ body {
animation: fadeInButton 0.5s ease-out; animation: fadeInButton 0.5s ease-out;
} }
/* === Navigation Styles === */
nav.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
background-color: #1a001a;
border-bottom: 2px solid #3d003d;
position: sticky;
top: 0;
z-index: 999;
}
.navbar .nav-logo {
color: #fff;
font-size: 1.5rem;
font-weight: bold;
text-decoration: none;
}
.navbar .nav-links {
display: flex;
gap: 1.5rem;
}
.navbar .nav-link {
color: #fff;
text-decoration: none;
font-size: 1rem;
transition: color 0.3s ease;
}
.navbar .nav-link:hover {
color: #ff99ff;
}
/* Optional: mobile responsiveness */
@media (max-width: 768px) {
.navbar {
flex-direction: column;
align-items: flex-start;
padding: 1rem;
}
.navbar .nav-links {
flex-direction: column;
gap: 1rem;
width: 100%;
}
.navbar .nav-link {
width: 100%;
}
}
+15 -5
View File
@@ -1,5 +1,7 @@
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Nav from "./components/Nav";
import Footer from "./components/Footer";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -12,17 +14,25 @@ const geistMono = Geist_Mono({
}); });
export const metadata = { export const metadata = {
title: "Create Next App", title: "Fretux Portfolio",
description: "Generated by create next app", description: "Frederik Spirgi – Full-stack developer",
}; };
export default function RootLayout({ children }) { export default function RootLayout({ children }) {
return ( return (
<html lang="en"> <html lang="en">
<body <body style={{
className={`${geistSans.variable} ${geistMono.variable} antialiased`} display: "flex",
> flexDirection: "column",
minHeight: "100vh",
background: "linear-gradient(135deg, #000, #1a001a)",
color: "#fff",
}}>
<Nav />
<main style={{ flex: 1 }}>
{children} {children}
</main>
<Footer />
</body> </body>
</html> </html>
); );