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

@@ -1,32 +0,0 @@
import Link from "next/link";
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">
<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>
</div>
<p className="mt-6">© Dennis Konkol 2025</p>
</footer>
);
}

View File

@@ -3,9 +3,8 @@
import {useParams, useRouter} from "next/navigation";
import {useEffect, useState} from "react";
import Link from "next/link";
import Header from "../../components/Header";
import Footer from "./Footer";
import Footer_Back from "../../components/Footer_Back";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
@@ -25,6 +24,13 @@ export default function ProjectDetail() {
const router = useRouter();
const {slug} = params as { slug: string };
const [project, setProject] = useState<Project | null>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
setTimeout(() => {
setIsVisible(true);
}, 150); // Delay to start the animation
}, []);
useEffect(() => {
if (slug) {
@@ -65,7 +71,7 @@ export default function ProjectDetail() {
}
return (
<div className="min-h-screen flex flex-col bg-radiant">
<div className={`min-h-screen flex flex-col bg-radiant ${isVisible ? 'animate-fly-in' : 'opacity-0'}`}>
<Header/>
<div className="flex-grow p-10 pt-24">
<div
@@ -75,15 +81,9 @@ export default function ProjectDetail() {
{project.text}
</ReactMarkdown>
</div>
<div className={"mt-10"}>
<button
className={"md:w-1/6 p-3 text-white bg-gradient-to-r from-blue-500 to-purple-500 rounded-xl hover:from-blue-600 hover:to-purple-600 transition"}>
<Link href="/">Back to Projects</Link>
</button>
</div>
</div>
</div>
<Footer/>
<Footer_Back/>
</div>
);
}

76
app/api/og/route.tsx Normal file
View File

@@ -0,0 +1,76 @@
import {ImageResponse} from 'next/og';
export async function GET() {
return new ImageResponse(
(
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
width: '1200px',
height: '630px',
background: 'radial-gradient(circle at 20% 20%, #ff8185, transparent 80%),' +
'radial-gradient(circle at 80% 80%, #ffaa91, transparent 80%)',
backgroundSize: '100% 100%',
color: '#333',
fontFamily: 'Arial, sans-serif',
padding: '20px',
}}
>
<div
style={{
position: 'absolute',
top: '20px',
left: '20px',
fontSize: '24px',
fontWeight: 'bold',
padding: '10px',
}}
>
Dennis Konkol | Portfolio
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'center',
textAlign: 'left',
padding: '20px',
backgroundColor: 'rgba(182,182,182,0.8)',
borderRadius: '10px',
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)',
maxWidth: '60%',
backdropFilter: 'blur(5px)',
}}
>
<h1 style={{fontSize: '48px', margin: '0'}}>Hi, Im Dennis</h1>
<h2 style={{fontSize: '32px', margin: '10px 0'}}>Student & Software Engineer</h2>
<p style={{fontSize: '24px', margin: '10px 0'}}>
Based in Osnabrück, Germany
</p>
<p style={{fontSize: '20px', margin: '10px 0'}}>
Passionate about technology, coding, and solving real-world problems.
</p>
</div>
<img
src="https://dki.one/images/me.jpg"
alt="Image of Dennis"
style={{
width: '400px',
height: '400px',
borderRadius: '10px',
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)',
objectFit: 'cover',
}}
/>
</div>
),
{
width: 1200,
height: 630,
}
);
}

View File

@@ -4,12 +4,16 @@ import path from 'path';
import matter from 'gray-matter';
export async function GET() {
try {
const projectsDirectory = path.join(process.cwd(), 'public/projects');
const filenames = fs.readdirSync(projectsDirectory);
console.log('Filenames:', filenames);
const projects = filenames.map((filename) => {
const projects = filenames
.filter((filename) => {
const filePath = path.join(projectsDirectory, filename);
return fs.statSync(filePath).isFile();
})
.map((filename) => {
const filePath = path.join(projectsDirectory, filename);
const fileContents = fs.readFileSync(filePath, 'utf8');
const {data} = matter(fileContents);
@@ -23,4 +27,8 @@ export async function GET() {
});
return NextResponse.json(projects);
} catch (error) {
console.error("Failed to fetch projects:", error);
return NextResponse.json({error: 'Failed to fetch projects'}, {status: 500});
}
}

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

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

View File

@@ -119,3 +119,68 @@ body {
.flex-grow {
flex-grow: 1;
}
.react-cookie-consent .content-wrapper {
flex: 1;
margin-right: 1rem;
}
.react-cookie-consent .button-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
@media (min-width: 768px) {
.react-cookie-consent .button-wrapper {
flex-direction: row;
}
}
.transition-underline {
position: relative;
display: inline-block;
}
.transition-underline::after {
content: '';
position: absolute;
left: 0;
bottom: -2px;
width: 100%;
height: 2px;
background-color: currentColor;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.transition-underline:hover::after {
opacity: 1;
transform: translateY(0);
}
.fade-in {
opacity: 1 !important;
transition: opacity 0.5s ease;
}
.fade-out {
opacity: 0;
transition: opacity 0.5s ease;
}
@keyframes flyIn {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.animate-fly-in {
animation: flyIn 1s ease-in-out;
}

View File

@@ -1,32 +1,45 @@
// app/layout.tsx
import type {Metadata} from "next";
"use client";
import {SpeedInsights} from "@vercel/speed-insights/next";
import {Analytics} from "@vercel/analytics/next";
import "./globals.css";
import {Roboto} from 'next/font/google'
import React from "react";
import {Roboto} from 'next/font/google';
import React, {useEffect, useState} from "react";
import ClientCookieConsentBanner from "./components/ClientCookieConsentBanner";
const roboto = Roboto({
variable: '--font-roboto',
weight: '400',
subsets: ['latin'],
})
export const metadata: Metadata = {
title: "Dennis",
description: "A portfolio website showcasing my work and skills.",
};
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const [consent, setConsent] = useState<string | null>(null);
useEffect(() => {
const storedConsent = localStorage.getItem("CookieConsent");
setConsent(storedConsent);
}, []);
const handleConsentChange = (newConsent: string) => {
setConsent(newConsent);
};
return (
<html lang="en">
<body className={roboto.variable}>
<ClientCookieConsentBanner onConsentChange={handleConsentChange}/>
{children}
{consent === "accepted" && <SpeedInsights/>}
{consent === "accepted" && <Analytics/>}
</body>
</html>
);

31
app/metadata.tsx Normal file
View File

@@ -0,0 +1,31 @@
// app/metadata.ts
import {Metadata} from "next";
export const metadata: Metadata = {
title: "Dennis Konkol | Portfolio",
description: "Portfolio of Dennis Konkol, a student and software engineer based in Osnabrück, Germany. Passionate about technology, coding, and solving real-world problems.",
keywords: ["Dennis Konkol", "Software Engineer", "Portfolio", "Student"],
authors: [{name: "Dennis Konkol", url: "https://dki.one"}],
openGraph: {
title: "Dennis Konkol | Portfolio",
description: "Explore my projects and get in touch!",
url: "https://dki.one",
siteName: "Dennis Konkol Portfolio",
images: [
{
url: "https://dki.one/api/og",
width: 1200,
height: 630,
alt: "Dennis Konkol Portfolio",
},
],
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Dennis Konkol | Portfolio",
description: "Student & Software Engineer based in Osnabrück, Germany.",
images: ["https://dki.one/api/og"],
},
};

View File

@@ -1,3 +1,4 @@
// app/page.tsx
"use client";
import Header from "./components/Header";
@@ -5,10 +6,34 @@ import Hero from "./components/Hero";
import Projects from "./components/Projects";
import Contact from "./components/Contact";
import Footer from "./components/Footer";
import Script from "next/script";
export default function Home() {
return (
<div className="min-h-screen flex flex-col bg-radiant-animated">
<Script
id={"structured-data"}
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "Person",
"name": "Dennis Konkol",
"url": "https://dki.one",
"jobTitle": "Software Engineer",
"address": {
"@type": "PostalAddress",
"addressLocality": "Osnabrück",
"addressCountry": "Germany",
},
"sameAs": [
"https://github.com/Denshooter",
"https://linkedin.com/in/dkonkol",
],
}),
}}
/>
<Header/>
<div className="h-10"></div>
<main>

View File

@@ -0,0 +1,66 @@
'use client';
import React, {useEffect, useState} from "react";
import Header from "../components/Header";
import Footer_Back from "../components/Footer_Back";
export default function PrivacyPolicy() {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
setTimeout(() => {
setIsVisible(true);
}, 350);
}, []);
return (
<div className={`min-h-screen flex flex-col bg-radiant-animated ${isVisible ? 'animate-fly-in' : 'opacity-0'}`}>
<Header/>
<div className="h-10"></div>
<main className="flex-grow p-10">
<h1 className="text-3xl font-bold">Privacy Policy</h1>
<p className="mt-4">
This Privacy Policy explains how I collect, use, and protect your information when you use my
website.
</p>
<h2 className="text-2xl font-semibold mt-6">Information We Collect</h2>
<p className="mt-2">
I use Vercel's Speed Insights and Web Analytics to collect information about the usage of my
website.
This includes data such as page views, time spent on pages, interaction metrics, and performance
insights.
</p>
<h2 className="text-2xl font-semibold mt-6">How I Use Your Information</h2>
<p className="mt-2">
The information collected is used to improve the performance, usability, and user experience of my
website.
I analyze this data to optimize content, and enhance site performance.
</p>
<h2 className="text-2xl font-semibold mt-6">Third-Party Services</h2>
<p className="mt-2">
Vercel may process collected data in accordance with their own privacy policies. Please review
<a href="https://vercel.com/legal/privacy-policy" className="text-blue-500 underline"> Vercel's
Privacy Policy</a> for more details.
</p>
<h2 className="text-2xl font-semibold mt-6">Data Protection</h2>
<p className="mt-2">
I take data security seriously. The information collected through Vercel's Speed Insights and Web
Analytics
is securely stored and only accessible to me.
</p>
<h2 className="text-2xl font-semibold mt-6">Your Rights</h2>
<p className="mt-2">
You have the right to request information about your data, ask for corrections, or request its
deletion.
If you wish to exercise these rights, please contact me.
</p>
<h2 className="text-2xl font-semibold mt-6">Contact Us</h2>
<p className="mt-2">
If you have any questions about this Privacy Policy, please contact me at info@dki.one or use the
contact form.
</p>
</main>
<Footer_Back/>
</div>
);
}

31
app/sitemap.tsx Normal file
View File

@@ -0,0 +1,31 @@
import {MetadataRoute} from "next";
import fs from "fs";
import path from "path";
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = "https://dki.one";
// Static pages
const staticRoutes = [
{url: `${baseUrl}/`, lastModified: new Date().toISOString()},
{url: `${baseUrl}/privacy-policy`, lastModified: new Date().toISOString()},
];
// Read project markdown files from the public folder
const projectsDirectory = path.join(process.cwd(), "public/projects");
let projectRoutes: { url: string; lastModified: string; }[] = [];
if (fs.existsSync(projectsDirectory)) {
const projectFiles = fs.readdirSync(projectsDirectory).filter(file => file.endsWith(".md"));
projectRoutes = projectFiles.map((file) => {
const slug = file.replace(".md", "");
return {
url: `${baseUrl}/projects/${slug}`,
lastModified: new Date().toISOString(),
};
});
}
return [...staticRoutes, ...projectRoutes];
}

304
package-lock.json generated
View File

@@ -9,10 +9,14 @@
"version": "0.1.0",
"dependencies": {
"@prisma/client": "^6.1.0",
"@vercel/analytics": "^1.4.1",
"@vercel/og": "^0.6.5",
"@vercel/speed-insights": "^1.1.0",
"gray-matter": "^4.0.3",
"next": "15.1.3",
"prisma": "^6.1.0",
"react": "^19.0.0",
"react-cookie-consent": "^9.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^9.0.3",
"rehype-raw": "^7.0.0",
@@ -949,6 +953,15 @@
"@prisma/debug": "6.3.0"
}
},
"node_modules/@resvg/resvg-wasm": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz",
"integrity": "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==",
"license": "MPL-2.0",
"engines": {
"node": ">= 10"
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -963,6 +976,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/@shuding/opentype.js": {
"version": "1.4.0-beta.0",
"resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz",
"integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==",
"license": "MIT",
"dependencies": {
"fflate": "^0.7.3",
"string.prototype.codepointat": "^0.2.1"
},
"bin": {
"ot": "bin/ot"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -1317,6 +1346,93 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
"node_modules/@vercel/analytics": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.4.1.tgz",
"integrity": "sha512-ekpL4ReX2TH3LnrRZTUKjHHNpNy9S1I7QmS+g/RQXoSUQ8ienzosuX7T9djZ/s8zPhBx1mpHP/Rw5875N+zQIQ==",
"license": "MPL-2.0",
"peerDependencies": {
"@remix-run/react": "^2",
"@sveltejs/kit": "^1 || ^2",
"next": ">= 13",
"react": "^18 || ^19 || ^19.0.0-rc",
"svelte": ">= 4",
"vue": "^3",
"vue-router": "^4"
},
"peerDependenciesMeta": {
"@remix-run/react": {
"optional": true
},
"@sveltejs/kit": {
"optional": true
},
"next": {
"optional": true
},
"react": {
"optional": true
},
"svelte": {
"optional": true
},
"vue": {
"optional": true
},
"vue-router": {
"optional": true
}
}
},
"node_modules/@vercel/og": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/@vercel/og/-/og-0.6.5.tgz",
"integrity": "sha512-GFXtgid3+TcVHTd668a10vGpzAh4Ty/yBZPRxKf1UicI8Vi8EthfvSxcaLW0KvQBBe1+d7TcjecLZHRT8JzQ4g==",
"license": "MPL-2.0",
"dependencies": {
"@resvg/resvg-wasm": "2.4.0",
"satori": "0.12.1",
"yoga-wasm-web": "0.3.3"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@vercel/speed-insights": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@vercel/speed-insights/-/speed-insights-1.1.0.tgz",
"integrity": "sha512-rAXxuhhO4mlRGC9noa5F7HLMtGg8YF1zAN6Pjd1Ny4pII4cerhtwSG4vympbCl+pWkH7nBS9kVXRD4FAn54dlg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peerDependencies": {
"@sveltejs/kit": "^1 || ^2",
"next": ">= 13",
"react": "^18 || ^19 || ^19.0.0-rc",
"svelte": ">= 4",
"vue": "^3",
"vue-router": "^4"
},
"peerDependenciesMeta": {
"@sveltejs/kit": {
"optional": true
},
"next": {
"optional": true
},
"react": {
"optional": true
},
"svelte": {
"optional": true
},
"vue": {
"optional": true
},
"vue-router": {
"optional": true
}
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@@ -1658,6 +1774,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -1776,6 +1901,15 @@
"node": ">= 6"
}
},
"node_modules/camelize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001696",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz",
@@ -1938,7 +2072,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/color-string": {
@@ -1994,6 +2127,47 @@
"node": ">= 8"
}
},
"node_modules/css-background-parser": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz",
"integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==",
"license": "MIT"
},
"node_modules/css-box-shadow": {
"version": "1.0.0-3",
"resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz",
"integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==",
"license": "MIT"
},
"node_modules/css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
"license": "ISC",
"engines": {
"node": ">=4"
}
},
"node_modules/css-gradient-parser": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz",
"integrity": "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==",
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/css-to-react-native": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
"license": "MIT",
"dependencies": {
"camelize": "^1.0.0",
"css-color-keywords": "^1.0.0",
"postcss-value-parser": "^4.0.2"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -2432,6 +2606,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -3003,6 +3183,12 @@
"reusify": "^1.0.4"
}
},
"node_modules/fflate": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz",
"integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==",
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -3607,6 +3793,18 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hex-rgb": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz",
"integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/html-url-attributes": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@@ -4226,6 +4424,12 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==",
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4362,6 +4566,16 @@
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"license": "MIT",
"dependencies": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -5686,6 +5900,12 @@
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
"license": "MIT"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -5699,6 +5919,16 @@
"node": ">=6"
}
},
"node_modules/parse-css-color": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz",
"integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==",
"license": "MIT",
"dependencies": {
"color-name": "^1.1.4",
"hex-rgb": "^4.1.0"
}
},
"node_modules/parse-entities": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
@@ -5976,7 +6206,6 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/prelude-ls": {
@@ -6078,6 +6307,21 @@
"node": ">=0.10.0"
}
},
"node_modules/react-cookie-consent": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/react-cookie-consent/-/react-cookie-consent-9.0.0.tgz",
"integrity": "sha512-Blyj+m+Zz7SFHYqT18p16EANgnSg2sIyU6Yp3vk83AnOnSW7qnehPkUe4+8+qxztJrNmCH5GP+VHsWzAKVOoZA==",
"license": "MIT",
"dependencies": {
"js-cookie": "^2.2.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/react-dom": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
@@ -6402,6 +6646,34 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/satori": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/satori/-/satori-0.12.1.tgz",
"integrity": "sha512-0SbjchvDrDbeXeQgxWVtSWxww7qcFgk3DtSE2/blHOSlLsSHwIqO2fCrtVa/EudJ7Eqno8A33QNx56rUyGbLuw==",
"license": "MPL-2.0",
"dependencies": {
"@shuding/opentype.js": "1.4.0-beta.0",
"css-background-parser": "^0.1.0",
"css-box-shadow": "1.0.0-3",
"css-gradient-parser": "^0.0.16",
"css-to-react-native": "^3.0.0",
"emoji-regex": "^10.2.1",
"escape-html": "^1.0.3",
"linebreak": "^1.1.0",
"parse-css-color": "^0.2.1",
"postcss-value-parser": "^4.2.0",
"yoga-wasm-web": "^0.3.3"
},
"engines": {
"node": ">=16"
}
},
"node_modules/satori/node_modules/emoji-regex": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
@@ -6749,6 +7021,12 @@
"node": ">=8"
}
},
"node_modules/string.prototype.codepointat": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
"integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==",
"license": "MIT"
},
"node_modules/string.prototype.includes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@@ -7130,6 +7408,12 @@
"node": ">=0.8"
}
},
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
"license": "MIT"
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -7333,6 +7617,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"license": "MIT",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unified": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
@@ -7724,6 +8018,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yoga-wasm-web": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz",
"integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==",
"license": "MIT"
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",

View File

@@ -10,14 +10,18 @@
},
"dependencies": {
"@prisma/client": "^6.1.0",
"@vercel/analytics": "^1.4.1",
"@vercel/speed-insights": "^1.1.0",
"@vercel/og": "^0.6.5",
"react-cookie-consent": "^9.0.0",
"gray-matter": "^4.0.3",
"next": "15.1.3",
"prisma": "^6.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^9.0.3",
"gray-matter": "^4.0.3",
"remark-gfm": "^4.0.0",
"rehype-raw": "^7.0.0"
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

3
public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
User-agent: *
Allow: /
Sitemap: https://dki.one/sitemap.xml