feat: add cookie consent banner and privacy policy page; update dependencies and improve animations
This commit is contained in:
13
app/components/ClientCookieConsentBanner.tsx
Normal file
13
app/components/ClientCookieConsentBanner.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
// app/components/ClientCookieConsentBanner.tsx
|
||||
|
||||
"use client";
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const CookieConsentBanner = dynamic(() => import('./CookieConsentBanner'), {ssr: false});
|
||||
|
||||
const ClientCookieConsentBanner = ({onConsentChange}: { onConsentChange: (consent: string) => void }) => {
|
||||
return <CookieConsentBanner onConsentChange={onConsentChange}/>;
|
||||
};
|
||||
|
||||
export default ClientCookieConsentBanner;
|
||||
@@ -1,55 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import {useState} from "react";
|
||||
// app/components/Contact.tsx
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
export default function Contact() {
|
||||
const [form, setForm] = useState({name: "", email: "", message: ""});
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
setForm({...form, [e.target.name]: e.target.value});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// Replace this with actual form submission logic (e.g., API call)
|
||||
try {
|
||||
// Simulate a successful submission
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
setSuccess(true);
|
||||
setForm({name: "", email: "", message: ""});
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError("Failed to send message. Please try again.");
|
||||
}
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
}, 350); // Delay to start the animation after Projects
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section id="contact" className="p-10">
|
||||
<section id="contact" className={`p-10 ${isVisible ? 'animate-fly-in' : 'opacity-0'}`}>
|
||||
<h2 className="text-3xl font-bold text-center text-gray-800 dark:text-white">
|
||||
Contact Me
|
||||
</h2>
|
||||
<div
|
||||
className="flex flex-col items-center p-8 bg-gradient-to-br from-white/60 to-white/30 backdrop-blur-lg rounded-2xl shadow-xl max-w-lg mx-auto mt-6">
|
||||
<form onSubmit={handleSubmit} className="w-full space-y-4">
|
||||
{success && (
|
||||
<p className="text-green-500">
|
||||
Your message has been sent successfully!
|
||||
</p>
|
||||
)}
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
<form className="w-full space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
className="w-full p-2 border rounded dark:text-white"
|
||||
required
|
||||
value={form.name}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<input
|
||||
type="email"
|
||||
@@ -57,8 +31,6 @@ export default function Contact() {
|
||||
placeholder="Email"
|
||||
className="w-full p-2 border rounded dark:text-white"
|
||||
required
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<textarea
|
||||
name="message"
|
||||
@@ -66,8 +38,6 @@ export default function Contact() {
|
||||
className="w-full p-2 border rounded dark:text-white"
|
||||
rows={5}
|
||||
required
|
||||
value={form.message}
|
||||
onChange={handleChange}
|
||||
></textarea>
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
98
app/components/CookieConsentBanner.tsx
Normal file
98
app/components/CookieConsentBanner.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import CookieConsent from "react-cookie-consent";
|
||||
import Link from "next/link";
|
||||
|
||||
const CookieConsentBanner = ({onConsentChange}: { onConsentChange: (consent: string) => void }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isFadingIn, setIsFadingIn] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const consent = localStorage.getItem("CookieConsent");
|
||||
if (!consent) {
|
||||
setIsVisible(true);
|
||||
setTimeout(() => setIsFadingIn(true), 10); // Delay to trigger CSS transition
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAccept = () => {
|
||||
setIsFadingIn(false);
|
||||
setTimeout(() => {
|
||||
localStorage.setItem("CookieConsent", "accepted");
|
||||
setIsVisible(false);
|
||||
onConsentChange("accepted");
|
||||
}, 500); // Match the duration of the fade-out transition
|
||||
};
|
||||
|
||||
const handleDecline = () => {
|
||||
setIsFadingIn(false);
|
||||
setTimeout(() => {
|
||||
localStorage.setItem("CookieConsent", "declined");
|
||||
setIsVisible(false);
|
||||
onConsentChange("declined");
|
||||
}, 500); // Match the duration of the fade-out transition
|
||||
};
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CookieConsent
|
||||
location="bottom"
|
||||
buttonText="Accept All"
|
||||
declineButtonText="Decline"
|
||||
enableDeclineButton
|
||||
cookieName="CookieConsent"
|
||||
containerClasses={`${isFadingIn ? 'fade-in' : 'fade-out'}`}
|
||||
style={{
|
||||
background: "rgba(211,211,211,0.44)",
|
||||
color: "#333",
|
||||
boxShadow: "0 4px 8px rgba(0, 0, 0, 0.1)",
|
||||
padding: "1rem",
|
||||
borderRadius: "8px",
|
||||
backdropFilter: "blur(10px)",
|
||||
margin: "2rem",
|
||||
width: "calc(100% - 4rem)",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
textAlign: "left",
|
||||
transition: "opacity 0.5s ease",
|
||||
opacity: isFadingIn ? 1 : 0,
|
||||
}}
|
||||
buttonWrapperClasses="button-wrapper"
|
||||
buttonStyle={{
|
||||
backgroundColor: "#4CAF50",
|
||||
color: "#FFF",
|
||||
fontSize: "14px",
|
||||
borderRadius: "4px",
|
||||
padding: "0.5rem 1rem",
|
||||
margin: "0.5rem",
|
||||
width: "100%",
|
||||
maxWidth: "200px",
|
||||
}}
|
||||
declineButtonStyle={{
|
||||
backgroundColor: "#f44336",
|
||||
color: "#FFF",
|
||||
fontSize: "14px",
|
||||
borderRadius: "4px",
|
||||
padding: "0.5rem 1rem",
|
||||
margin: "0.5rem",
|
||||
width: "100%",
|
||||
maxWidth: "200px",
|
||||
}}
|
||||
expires={90}
|
||||
onAccept={handleAccept}
|
||||
onDecline={handleDecline}
|
||||
>
|
||||
<div className="content-wrapper text-xl">
|
||||
This website uses cookies to enhance your experience. By using our website, you consent to the use of
|
||||
cookies.
|
||||
You can read more in our <Link href="/" className="text-blue-800 transition-underline">privacy
|
||||
policy</Link>.
|
||||
</div>
|
||||
</CookieConsent>
|
||||
);
|
||||
};
|
||||
|
||||
export default CookieConsentBanner;
|
||||
@@ -1,6 +1,15 @@
|
||||
import Link from "next/link";
|
||||
import {useEffect, useState} from "react";
|
||||
|
||||
export default function Footer() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
}, 450); // Delay to start the animation
|
||||
}, []);
|
||||
|
||||
const scrollToSection = (id: string) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
@@ -10,33 +19,40 @@ export default function Footer() {
|
||||
|
||||
return (
|
||||
<footer
|
||||
className="p-10 bg-gradient-to-br from-white/60 to-white/30 backdrop-blur-lg rounded-2xl shadow-xl text-center text-gray-800 dark:text-white">
|
||||
<h1 className="text-3xl font-bold">Thank You for Visiting</h1>
|
||||
<p className="mt-4 text-xl">Connect with me on social platforms:</p>
|
||||
<div className="flex justify-center space-x-4 mt-4">
|
||||
<Link href="https://github.com/Denshooter" target="_blank">
|
||||
<svg
|
||||
className="w-6 h-6 text-gray-700 dark:text-white hover:text-gray-900 dark:hover:text-gray-300 transition"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.387.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.757-1.333-1.757-1.09-.746.083-.73.083-.73 1.205.084 1.84 1.237 1.84 1.237 1.07 1.835 2.807 1.305 3.492.997.108-.774.42-1.305.763-1.605-2.665-.305-5.466-1.332-5.466-5.93 0-1.31.467-2.38 1.235-3.22-.123-.303-.535-1.527.117-3.18 0 0 1.008-.322 3.3 1.23.957-.266 1.98-.4 3-.405 1.02.005 2.043.14 3 .405 2.29-1.552 3.297-1.23 3.297-1.23.653 1.653.24 2.877.118 3.18.77.84 1.233 1.91 1.233 3.22 0 4.61-2.803 5.62-5.475 5.92.43.37.823 1.1.823 2.22v3.293c0 .32.218.694.825.577C20.565 21.8 24 17.3 24 12c0-6.63-5.37-12-12-12z"/>
|
||||
</svg>
|
||||
</Link>
|
||||
<Link href="https://linkedin.com/in/dkonkol" target="_blank">
|
||||
<svg
|
||||
className="w-6 h-6 text-gray-700 dark:text-white hover:text-gray-900 dark:hover:text-gray-300 transition"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M19 0h-14c-2.76 0-5 2.24-5 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5v-14c0-2.76-2.24-5-5-5zm-11 19h-3v-10h3v10zm-1.5-11.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5 11.5h-3v-5.5c0-1.38-1.12-2.5-2.5-2.5s-2.5 1.12-2.5 2.5v5.5h-3v-10h3v1.5c.83-1.17 2.17-1.5 3.5-1.5 2.48 0 4.5 2.02 4.5 4.5v5.5z"/>
|
||||
</svg>
|
||||
</Link>
|
||||
className={`p-3 bg-gradient-to-br from-white/60 to-white/30 backdrop-blur-lg rounded-2xl shadow-xl text-center text-gray-800 ${isVisible ? 'animate-fly-in' : 'opacity-0'}`}>
|
||||
<div className={`flex flex-col md:flex-row items-center justify-between`}>
|
||||
<div className={`flex-col items-center`}>
|
||||
<h1 className="md:text-xl font-bold">Thank You for Visiting</h1>
|
||||
<p className="md:mt-1 text-lg">Connect with me on social platforms:</p>
|
||||
<div className="flex justify-center items-center space-x-4 mt-4">
|
||||
<Link href="https://github.com/Denshooter" target="_blank">
|
||||
<svg className="w-10 h-10" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.387.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.757-1.333-1.757-1.09-.746.083-.73.083-.73 1.205.084 1.84 1.237 1.84 1.237 1.07 1.835 2.807 1.305 3.492.997.108-.774.42-1.305.763-1.605-2.665-.305-5.466-1.332-5.466-5.93 0-1.31.467-2.38 1.235-3.22-.123-.303-.535-1.527.117-3.18 0 0 1.008-.322 3.3 1.23.957-.266 1.98-.4 3-.405 1.02.005 2.043.14 3 .405 2.29-1.552 3.297-1.23 3.297-1.23.653 1.653.24 2.877.118 3.18.77.84 1.233 1.91 1.233 3.22 0 4.61-2.803 5.62-5.475 5.92.43.37.823 1.1.823 2.22v3.293c0 .32.218.694.825.577C20.565 21.8 24 17.3 24 12c0-6.63-5.37-12-12-12z"/>
|
||||
</svg>
|
||||
</Link>
|
||||
<Link href="https://linkedin.com/in/dkonkol" target="_blank">
|
||||
<svg className="w-10 h-10" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M19 0h-14c-2.76 0-5 2.24-5 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5v-14c0-2.76-2.24-5-5-5zm-11 19h-3v-10h3v10zm-1.5-11.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5 11.5h-3v-5.5c0-1.38-1.12-2.5-2.5-2.5s-2.5 1.12-2.5 2.5v5.5h-3v-10h3v1.5c.83-1.17 2.17-1.5 3.5-1.5 2.48 0 4.5 2.02 4.5 4.5v5.5z"/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 md:absolute md:left-1/2 md:transform md:-translate-x-1/2">
|
||||
<button onClick={() => scrollToSection("about")}
|
||||
className="p-4 mt-4 md:px-4 md:my-6 text-white bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl hover:from-blue-600 hover:to-purple-600 transition">
|
||||
Back to Top
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-col">
|
||||
<div className="mt-4">
|
||||
<Link href="/privacy-policy" className="text-blue-800 transition-underline">Privacy
|
||||
Policy</Link>
|
||||
</div>
|
||||
<p className="md:mt-4">© Dennis Konkol 2025</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-6">© Dennis Konkol 2025</p>
|
||||
<button
|
||||
onClick={() => scrollToSection("about")}
|
||||
className="mt-6 inline-block px-6 py-2 text-white bg-gradient-to-r from-blue-500 to-purple-500 rounded hover:from-blue-600 hover:to-purple-600 transition">
|
||||
Back to Top
|
||||
</button>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
50
app/components/Footer_Back.tsx
Normal file
50
app/components/Footer_Back.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import Link from "next/link";
|
||||
import {useEffect, useState} from "react";
|
||||
|
||||
export default function Footer_Back() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
}, 450); // Delay to start the animation
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<footer
|
||||
className={`p-3 bg-gradient-to-br from-white/60 to-white/30 backdrop-blur-lg rounded-2xl shadow-xl text-center text-gray-800 ${isVisible ? 'animate-fly-in' : 'opacity-0'}`}>
|
||||
<div className={`flex flex-col md:flex-row items-center justify-between`}>
|
||||
<div className={`flex-col items-center`}>
|
||||
<p className="md:mt-1 text-lg">Connect with me on social platforms:</p>
|
||||
<div className="flex justify-center items-center space-x-4 mt-4">
|
||||
<Link href="https://github.com/Denshooter" target="_blank">
|
||||
<svg className="w-10 h-10" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.387.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.757-1.333-1.757-1.09-.746.083-.73.083-.73 1.205.084 1.84 1.237 1.84 1.237 1.07 1.835 2.807 1.305 3.492.997.108-.774.42-1.305.763-1.605-2.665-.305-5.466-1.332-5.466-5.93 0-1.31.467-2.38 1.235-3.22-.123-.303-.535-1.527.117-3.18 0 0 1.008-.322 3.3 1.23.957-.266 1.98-.4 3-.405 1.02.005 2.043.14 3 .405 2.29-1.552 3.297-1.23 3.297-1.23.653 1.653.24 2.877.118 3.18.77.84 1.233 1.91 1.233 3.22 0 4.61-2.803 5.62-5.475 5.92.43.37.823 1.1.823 2.22v3.293c0 .32.218.694.825.577C20.565 21.8 24 17.3 24 12c0-6.63-5.37-12-12-12z"/>
|
||||
</svg>
|
||||
</Link>
|
||||
<Link href="https://linkedin.com/in/dkonkol" target="_blank">
|
||||
<svg className="w-10 h-10" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M19 0h-14c-2.76 0-5 2.24-5 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5v-14c0-2.76-2.24-5-5-5zm-11 19h-3v-10h3v10zm-1.5-11.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5 11.5h-3v-5.5c0-1.38-1.12-2.5-2.5-2.5s-2.5 1.12-2.5 2.5v5.5h-3v-10h3v1.5c.83-1.17 2.17-1.5 3.5-1.5 2.48 0 4.5 2.02 4.5 4.5v5.5z"/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 md:absolute md:left-1/2 md:transform md:-translate-x-1/2">
|
||||
<Link href={"/"}
|
||||
className="p-4 mt-4 md:px-4 md:my-6 text-white bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl hover:from-blue-600 hover:to-purple-600 transition">
|
||||
Back to main page
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-col">
|
||||
<div className="mt-4">
|
||||
<Link href="/privacy-policy" className="text-blue-800 transition-underline">Privacy
|
||||
Policy</Link>
|
||||
</div>
|
||||
<p className="md:mt-4">© Dennis Konkol 2025</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import {useState} from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Header() {
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
}, 50); // Delay to start the animation after Projects
|
||||
}, []);
|
||||
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
@@ -21,7 +30,7 @@ export default function Header() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className={`p-4 ${isVisible ? 'animate-fly-in' : 'opacity-0'}`}>
|
||||
<div
|
||||
className={`fixed top-4 left-4 right-4 p-4 bg-white/45 text-gray-700 backdrop-blur-md shadow-xl rounded-2xl z-50 ${isSidebarOpen ? 'transform -translate-y-full' : ''}`}>
|
||||
<header className="w-full">
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
// app/components/Hero.tsx
|
||||
import React, {useEffect, useState} from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Hero() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
}, 150); // Delay to start the animation
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id="about"
|
||||
className="flex flex-col md:flex-row items-center justify-center pt-16 pb-16 px-6 text-gray-700">
|
||||
|
||||
{/* Left Section: Text */}
|
||||
<div className="flex flex-col items-center p-8 bg-gradient-to-br from-white/60 to-white/30
|
||||
backdrop-blur-lg rounded-2xl shadow-xl max-w-lg text-center">
|
||||
className={`flex flex-col md:flex-row items-center justify-center pt-16 pb-16 px-6 text-gray-700 ${isVisible ? 'animate-fly-in' : 'opacity-0'}`}>
|
||||
<div
|
||||
className="flex flex-col items-center p-8 bg-gradient-to-br from-white/60 to-white/30 backdrop-blur-lg rounded-2xl shadow-xl max-w-lg text-center">
|
||||
<h1 className="text-4xl md:text-5xl font-extrabold text-gray-900">
|
||||
Hi, I’m Dennis
|
||||
</h1>
|
||||
@@ -17,19 +25,15 @@ export default function Hero() {
|
||||
<h3 className="mt-1 text-lg md:text-xl text-gray-600">
|
||||
Based in Osnabrück, Germany
|
||||
</h3>
|
||||
|
||||
<p className="mt-6 text-gray-800 text-lg leading-relaxed">
|
||||
Passionate about technology, coding, and solving real-world problems.
|
||||
I enjoy building innovative solutions and continuously expanding my knowledge.
|
||||
</p>
|
||||
|
||||
<p className="mt-4 text-gray-700 text-base">
|
||||
Currently working on exciting projects that merge creativity with functionality.
|
||||
Always eager to learn and collaborate!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right Section: Image */}
|
||||
<div className="flex mt-8 md:mt-0 md:ml-12">
|
||||
<Image
|
||||
src="/images/me.jpg"
|
||||
@@ -41,4 +45,4 @@ export default function Hero() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
// app/components/Projects.tsx
|
||||
"use client";
|
||||
|
||||
import React, {useEffect, useState} from "react";
|
||||
import Link from "next/link";
|
||||
import {useEffect, useState} from "react";
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
@@ -13,37 +10,42 @@ interface Project {
|
||||
|
||||
export default function Projects() {
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/projects');
|
||||
try {
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch projects");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch projects:", error);
|
||||
}
|
||||
const projectsData = await response.json();
|
||||
setProjects(projectsData);
|
||||
setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
}, 250); // Delay to start the animation after Hero
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch projects:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProjects();
|
||||
fetchProjects().then(r => r);
|
||||
}, []);
|
||||
const numberOfProjects = projects.length;
|
||||
|
||||
return (
|
||||
<section id="projects" className="p-10">
|
||||
<section id="projects" className={`p-10 ${isVisible ? 'animate-fly-in' : 'opacity-0'}`}>
|
||||
<h2 className="text-3xl font-bold text-center text-gray-800">
|
||||
Projects
|
||||
</h2>
|
||||
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{projects.map((project) => (
|
||||
<Link
|
||||
key={project.id}
|
||||
href={`/Projects/${project.slug}`}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div
|
||||
key={project.id}
|
||||
className="p-4 border shadow-lg bg-white/45 rounded-2xl"
|
||||
>
|
||||
{projects.map((project, index) => (
|
||||
<Link key={project.id} href={`/Projects/${project.slug}`} className="cursor-pointer">
|
||||
<div className={`p-4 border shadow-lg bg-white/45 rounded-2xl animate-fly-in`}
|
||||
style={{animationDelay: `${index * 0.1}s`}}>
|
||||
<h3 className="text-2xl font-bold text-gray-800">
|
||||
{project.title}
|
||||
</h3>
|
||||
@@ -53,6 +55,15 @@ export default function Projects() {
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
<div className={`p-4 border shadow-lg bg-white/45 rounded-2xl animate-fly-in`}
|
||||
style={{animationDelay: `${(numberOfProjects + 1) * 0.1}s`}}>
|
||||
<h3 className="text-2xl font-bold text-gray-800">
|
||||
More to come
|
||||
</h3>
|
||||
<p className="mt-2 text-gray-500">
|
||||
...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user