feat: add cookie consent banner and privacy policy page; update dependencies and improve animations

This commit is contained in:
2025-02-04 16:44:49 +01:00
parent e37aba3ece
commit 36e44ef1b8
24 changed files with 929 additions and 168 deletions

View 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;

View File

@@ -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"

View 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;

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -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">

View File

@@ -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, Im 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>
);
}
}

View File

@@ -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>
);