- 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:
+99
-125
@@ -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",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
←
|
|
||||||
</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",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
→
|
|
||||||
</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,84 +189,82 @@ 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>
|
{adjustedSkills.filter((s) => s.level > 0).map((skill, sIndex) => (
|
||||||
|
<div
|
||||||
{/* SKILLS: show per stage */}
|
key={`${skill.name}-${index}`}
|
||||||
<div style={{display: "flex", gap: "2rem"}}>
|
className={currentSection === index ? "skill-card animate-skill" : "skill-card"}
|
||||||
<div style={{
|
style={{ textAlign: "center", width: "120px", animationDelay: `${sIndex * 100}ms`, animationFillMode: "both" }}
|
||||||
display: "flex",
|
>
|
||||||
gap: "2rem",
|
<p style={{ fontWeight: "bold" }}>{skill.name}</p>
|
||||||
flexWrap: "wrap",
|
<div style={{ padding: "0.5rem 1rem", backgroundColor: "#1a001a", border: "2px solid #3d003d", borderRadius: "10px", color: "#fff" }}>
|
||||||
justifyContent: "center"
|
{getSkillLabel(skill.level)}
|
||||||
}}>
|
|
||||||
{adjustedSkills.filter((s) => s.level > 0).map((skill, sIndex) => (
|
|
||||||
<div
|
|
||||||
key={`${skill.name}-${index}`}
|
|
||||||
className={currentSection === index ? "skill-card animate-skill" : "skill-card"}
|
|
||||||
style={{
|
|
||||||
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",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getSkillLabel(skill.level)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{/* End of scroll container */}
|
|
||||||
{timelineFinished && (
|
{currentSection > 0 && (
|
||||||
<div style={{marginTop: "3rem", textAlign: "center", width: "100%"}}>
|
<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" }}>
|
||||||
<Link href="/projects">
|
←
|
||||||
<button
|
</button>
|
||||||
className="learnMoreButton"
|
)}
|
||||||
style={{
|
{currentSection < timelineData.length - 1 && (
|
||||||
padding: "1rem 2rem",
|
<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" }}>
|
||||||
fontSize: "1.25rem",
|
→
|
||||||
backgroundColor: "#3d003d",
|
</button>
|
||||||
border: "none",
|
|
||||||
borderRadius: "5px",
|
|
||||||
color: "#fff",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "background-color 0.3s ease",
|
|
||||||
}}
|
|
||||||
onMouseOver={(e) => (e.currentTarget.style.backgroundColor = "#2e002e")}
|
|
||||||
onMouseOut={(e) => (e.currentTarget.style.backgroundColor = "#3d003d")}
|
|
||||||
>
|
|
||||||
View Projects
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
{timelineFinished && showScrollPrompt && (
|
||||||
|
<div className="scrollPrompt">
|
||||||
|
⬇ Scroll down to continue ⬇
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* FINAL BUTTON SECTION - MOVED OUTSIDE */}
|
||||||
|
{timelineFinished && (
|
||||||
|
// 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">
|
||||||
|
<button
|
||||||
|
className="learnMoreButton"
|
||||||
|
style={{
|
||||||
|
padding: "1rem 2rem",
|
||||||
|
fontSize: "1.25rem",
|
||||||
|
backgroundColor: "#3d003d",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "5px",
|
||||||
|
color: "#fff",
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "background-color 0.3s ease",
|
||||||
|
}}
|
||||||
|
onMouseOver={(e) => (e.currentTarget.style.backgroundColor = "#2e002e")}
|
||||||
|
onMouseOut={(e) => (e.currentTarget.style.backgroundColor = "#3d003d")}
|
||||||
|
>
|
||||||
|
View Projects
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-11
@@ -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,18 +14,26 @@ 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",
|
||||||
{children}
|
minHeight: "100vh",
|
||||||
</body>
|
background: "linear-gradient(135deg, #000, #1a001a)",
|
||||||
</html>
|
color: "#fff",
|
||||||
);
|
}}>
|
||||||
|
<Nav />
|
||||||
|
<main style={{ flex: 1 }}>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user