diff --git a/public/images/portfolio1.jpg b/public/images/portfolio1.jpg new file mode 100644 index 0000000..545e94a Binary files /dev/null and b/public/images/portfolio1.jpg differ diff --git a/public/images/portfolio2.jpg b/public/images/portfolio2.jpg new file mode 100644 index 0000000..545e94a Binary files /dev/null and b/public/images/portfolio2.jpg differ diff --git a/src/app/components/FadeInSection.js b/src/app/components/FadeInSection.js new file mode 100644 index 0000000..c80d999 --- /dev/null +++ b/src/app/components/FadeInSection.js @@ -0,0 +1,54 @@ +"use client"; + +import { useRef, useState, useEffect } from 'react'; + +const FadeInSection = ({ children }) => { + const domRef = useRef(); + const [hasBeenVisible, setHasBeenVisible] = useState(false); + const [opacity, setOpacity] = useState(0); + + useEffect(() => { + const thresholds = Array.from({ length: 101 }, (_, i) => i / 100); + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + // If 30% or more of the element is visible and it hasn't been set visible before... + if (entry.intersectionRatio >= 0.3 && !hasBeenVisible) { + setHasBeenVisible(true); + setOpacity(1); + } else if (!hasBeenVisible) { + // Gradually update opacity until the threshold is met + setOpacity(entry.intersectionRatio); + } + }); + }, + { + threshold: thresholds, + // A slight negative bottom margin delays the fade until more of the section is in view + rootMargin: "0px 0px -20% 0px", + } + ); + if (domRef.current) { + observer.observe(domRef.current); + } + return () => { + observer.disconnect(); + }; + }, [hasBeenVisible]); + + return ( +
+ {children} +
+ ); +}; + +export default FadeInSection; \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css index 045a4b0..e98ae2d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,3 +1,13 @@ +/* Global Styles */ +body { + background: linear-gradient(135deg, #000000, #1a001a); + font-size: 1.125rem; + font-family: 'Helvetica', sans-serif; + color: #ffffff; + margin: 0; + padding: 0; +} + .container { max-width: 1200px; margin: 0 auto; @@ -12,58 +22,86 @@ .title { font-size: 2.5rem; margin-bottom: 0.5rem; + color: #ffffff; +} + +/* Title animation classes */ +.title.normal { + opacity: 1; +} +.title.fadeOut { + animation: fadeOutAnimation 0.5s forwards; +} +.title.fadeIn { + animation: fadeInAnimation 0.5s forwards; +} + +@keyframes fadeOutAnimation { + from { opacity: 1; } + to { opacity: 0; } +} +@keyframes fadeInAnimation { + from { opacity: 0; } + to { opacity: 1; } } .intro { - font-size: 1.25rem; - color: #666; + font-size: 1.5rem; + color: #cccccc; +} + +/* Default fade-in section: occupies at least one full viewport */ +.fade-in-section { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transform: translateY(20px); + transition: opacity 0.3s ease-out, transform 0.3s ease-out; + will-change: opacity, transform; +} + +/* Custom override for the first content section */ +.firstSection { + min-height: auto; /* Remove full viewport height requirement */ + padding: 2rem 0; /* Add vertical padding if needed */ } .section { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 2rem; -} - -/* Section where text is on the left */ -.textLeft { - flex: 1; - padding: 1rem; -} - -/* Section where image is on the right */ -.imageRight { - flex: 1; - padding: 1rem; -} - -/* Section where image is on the left */ -.imageLeft { - flex: 1; - padding: 1rem; -} - -/* Section where button is on the right */ -.buttonRight { - flex: 1; - padding: 1rem; - display: flex; - justify-content: flex-end; -} - -/* Ensure images are responsive */ -.imageLeft img, -.imageRight img { width: 100%; +} + +.textLeft, +.imageRight, +.imageLeft, +.buttonLeft { + padding: 1rem; + flex: 1; +} + +/* Button container aligned to the left */ +.buttonLeft { + display: flex; + justify-content: flex-start; +} + +/* Slightly smaller images */ +.imageRight img, +.imageLeft img { + width: 90%; height: auto; border-radius: 8px; } +/* Enlarged Learn More button with dark purple tones */ .learnMoreButton { - padding: 0.75rem 1.5rem; - font-size: 1rem; - background-color: #0070f3; + padding: 1rem 2rem; + font-size: 1.25rem; + background-color: #3d003d; border: none; border-radius: 5px; color: #fff; @@ -72,5 +110,38 @@ } .learnMoreButton:hover { - background-color: #005bb5; -} \ No newline at end of file + background-color: #2e002e; +} + +/* Scroll Prompt styling with no background */ +.scrollPrompt { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + padding: 0.5rem 1rem; + border-radius: 5px; + color: #fff; + font-size: 1rem; + z-index: 1000; + background: none; +} + +/* When section is fully visible */ +.fade-in-section.is-visible { + opacity: 1; + transform: none; +} + +/* Hide scrollbar for Chrome, Safari, Opera */ +::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +body { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + + diff --git a/src/app/page.js b/src/app/page.js index 890dde9..f7af429 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,51 +1,101 @@ +"use client"; + +import { useState, useEffect } from "react"; import Image from "next/image"; +import Link from "next/link"; +import FadeInSection from "./components/FadeInSection"; export default function Home() { + const [showScrollPrompt, setShowScrollPrompt] = useState(false); + const [title, setTitle] = useState("Fretux"); + const [phase, setPhase] = useState("normal"); + + // Title flicker effect: every 3 seconds, fade out, toggle title, then fade in. + useEffect(() => { + const interval = setInterval(() => { + setPhase("fadeOut"); + setTimeout(() => { + setTitle((prev) => (prev === "Fretux" ? "Frederik Spirgi" : "Fretux")); + setPhase("fadeIn"); + setTimeout(() => { + setPhase("normal"); + }, 500); + }, 500); + }, 3000); + return () => clearInterval(interval); + }, []); + + // Show scroll prompt after a delay (once the title has loaded in). + useEffect(() => { + const timer = setTimeout(() => { + setShowScrollPrompt(true); + }, 1500); // Adjust the delay as needed + return () => clearTimeout(timer); + }, []); + + // Remove the scroll prompt permanently upon first scroll. + useEffect(() => { + const handleScroll = () => { + setShowScrollPrompt(false); + window.removeEventListener("scroll", handleScroll); + }; + + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + return ( -
- {/* Header with title and introduction */} -
-

My Portfolio

-

- Welcome to my personal portfolio. I am passionate about creating beautiful and efficient web experiences. -

-
+
+ {showScrollPrompt && ( +
+

Scroll subtly

+
+ )} - {/* Section 1: Text on left, image on right */} -
-
-

- I specialize in building responsive, dynamic web applications that deliver exceptional user experiences. + +

+

{title}

+

+ Welcome to my portfolio. I’m a web developer passionate about building modern, responsive applications.

-
-
- Project showcase -
-
+ + - {/* Section 2: Image on left, button on right */} -
-
- Another project -
-
- -
-
+ +
+
+

+ I specialize in creating intuitive user experiences and dynamic web solutions using Next.js, React, and modern design principles. +

+
+
+ Screenshot of a portfolio project +
+
+
+ + +
+
+ Another project snapshot +
+
+ + + +
+
+
); -} +} \ No newline at end of file