Merge pull request #1 from Denshooter/dev

feat: add cookie consent banner and privacy policy page; update depen…
This commit is contained in:
Denshooter
2025-02-04 16:45:42 +01:00
committed by GitHub
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 {useParams, useRouter} from "next/navigation";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import Link from "next/link";
import Header from "../../components/Header"; import Header from "../../components/Header";
import Footer from "./Footer"; import Footer_Back from "../../components/Footer_Back";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw"; import rehypeRaw from "rehype-raw";
@@ -25,6 +24,13 @@ export default function ProjectDetail() {
const router = useRouter(); const router = useRouter();
const {slug} = params as { slug: string }; const {slug} = params as { slug: string };
const [project, setProject] = useState<Project | null>(null); const [project, setProject] = useState<Project | null>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
setTimeout(() => {
setIsVisible(true);
}, 150); // Delay to start the animation
}, []);
useEffect(() => { useEffect(() => {
if (slug) { if (slug) {
@@ -65,7 +71,7 @@ export default function ProjectDetail() {
} }
return ( 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/> <Header/>
<div className="flex-grow p-10 pt-24"> <div className="flex-grow p-10 pt-24">
<div <div
@@ -75,15 +81,9 @@ export default function ProjectDetail() {
{project.text} {project.text}
</ReactMarkdown> </ReactMarkdown>
</div> </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>
</div> </div>
<Footer/> <Footer_Back/>
</div> </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,23 +4,31 @@ import path from 'path';
import matter from 'gray-matter'; import matter from 'gray-matter';
export async function GET() { export async function GET() {
const projectsDirectory = path.join(process.cwd(), 'public/projects'); try {
const filenames = fs.readdirSync(projectsDirectory); const projectsDirectory = path.join(process.cwd(), 'public/projects');
const filenames = fs.readdirSync(projectsDirectory);
console.log('Filenames:', filenames); 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);
const projects = filenames.map((filename) => { return {
const filePath = path.join(projectsDirectory, filename); id: data.id,
const fileContents = fs.readFileSync(filePath, 'utf8'); title: data.title,
const {data} = matter(fileContents); description: data.description,
slug: filename.replace('.md', ''),
};
});
return { return NextResponse.json(projects);
id: data.id, } catch (error) {
title: data.title, console.error("Failed to fetch projects:", error);
description: data.description, return NextResponse.json({error: 'Failed to fetch projects'}, {status: 500});
slug: filename.replace('.md', ''), }
};
});
return NextResponse.json(projects);
} }

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"; // app/components/Contact.tsx
import React, {useEffect, useState} from "react";
import {useState} from "react";
export default function Contact() { export default function Contact() {
const [form, setForm] = useState({name: "", email: "", message: ""}); const [isVisible, setIsVisible] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState("");
const handleChange = ( useEffect(() => {
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, setTimeout(() => {
) => { setIsVisible(true);
setForm({...form, [e.target.name]: e.target.value}); }, 350); // Delay to start the animation after Projects
}; }, []);
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.");
}
}
};
return ( 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"> <h2 className="text-3xl font-bold text-center text-gray-800 dark:text-white">
Contact Me Contact Me
</h2> </h2>
<div <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"> 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"> <form 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>}
<input <input
type="text" type="text"
name="name" name="name"
placeholder="Name" placeholder="Name"
className="w-full p-2 border rounded dark:text-white" className="w-full p-2 border rounded dark:text-white"
required required
value={form.name}
onChange={handleChange}
/> />
<input <input
type="email" type="email"
@@ -57,8 +31,6 @@ export default function Contact() {
placeholder="Email" placeholder="Email"
className="w-full p-2 border rounded dark:text-white" className="w-full p-2 border rounded dark:text-white"
required required
value={form.email}
onChange={handleChange}
/> />
<textarea <textarea
name="message" name="message"
@@ -66,8 +38,6 @@ export default function Contact() {
className="w-full p-2 border rounded dark:text-white" className="w-full p-2 border rounded dark:text-white"
rows={5} rows={5}
required required
value={form.message}
onChange={handleChange}
></textarea> ></textarea>
<button <button
type="submit" 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 Link from "next/link";
import {useEffect, useState} from "react";
export default function Footer() { export default function Footer() {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
setTimeout(() => {
setIsVisible(true);
}, 450); // Delay to start the animation
}, []);
const scrollToSection = (id: string) => { const scrollToSection = (id: string) => {
const element = document.getElementById(id); const element = document.getElementById(id);
if (element) { if (element) {
@@ -10,33 +19,40 @@ export default function Footer() {
return ( return (
<footer <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"> 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'}`}>
<h1 className="text-3xl font-bold">Thank You for Visiting</h1> <div className={`flex flex-col md:flex-row items-center justify-between`}>
<p className="mt-4 text-xl">Connect with me on social platforms:</p> <div className={`flex-col items-center`}>
<div className="flex justify-center space-x-4 mt-4"> <h1 className="md:text-xl font-bold">Thank You for Visiting</h1>
<Link href="https://github.com/Denshooter" target="_blank"> <p className="md:mt-1 text-lg">Connect with me on social platforms:</p>
<svg <div className="flex justify-center items-center space-x-4 mt-4">
className="w-6 h-6 text-gray-700 dark:text-white hover:text-gray-900 dark:hover:text-gray-300 transition" <Link href="https://github.com/Denshooter" target="_blank">
fill="currentColor" viewBox="0 0 24 24"> <svg className="w-10 h-10" fill="currentColor" viewBox="0 0 24 24">
<path <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"/> 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> </svg>
</Link> </Link>
<Link href="https://linkedin.com/in/dkonkol" target="_blank"> <Link href="https://linkedin.com/in/dkonkol" target="_blank">
<svg <svg className="w-10 h-10" fill="currentColor" viewBox="0 0 24 24">
className="w-6 h-6 text-gray-700 dark:text-white hover:text-gray-900 dark:hover:text-gray-300 transition" <path
fill="currentColor" viewBox="0 0 24 24"> 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"/>
<path </svg>
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"/> </Link>
</svg> </div>
</Link> </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> </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> </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"; "use client";
import {useState} from "react"; import {useEffect, useState} from "react";
import Link from "next/link"; import Link from "next/link";
export default function Header() { 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 [isSidebarOpen, setIsSidebarOpen] = useState(false);
const toggleSidebar = () => { const toggleSidebar = () => {
@@ -21,7 +30,7 @@ export default function Header() {
}; };
return ( return (
<div className="p-4"> <div className={`p-4 ${isVisible ? 'animate-fly-in' : 'opacity-0'}`}>
<div <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' : ''}`}> 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"> <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"; import Image from "next/image";
export default function Hero() { export default function Hero() {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
setTimeout(() => {
setIsVisible(true);
}, 150); // Delay to start the animation
}, []);
return ( return (
<div id="about" <div id="about"
className="flex flex-col md:flex-row items-center justify-center pt-16 pb-16 px-6 text-gray-700"> 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
{/* Left Section: Text */} 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">
<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"> <h1 className="text-4xl md:text-5xl font-extrabold text-gray-900">
Hi, Im Dennis Hi, Im Dennis
</h1> </h1>
@@ -17,19 +25,15 @@ export default function Hero() {
<h3 className="mt-1 text-lg md:text-xl text-gray-600"> <h3 className="mt-1 text-lg md:text-xl text-gray-600">
Based in Osnabrück, Germany Based in Osnabrück, Germany
</h3> </h3>
<p className="mt-6 text-gray-800 text-lg leading-relaxed"> <p className="mt-6 text-gray-800 text-lg leading-relaxed">
Passionate about technology, coding, and solving real-world problems. Passionate about technology, coding, and solving real-world problems.
I enjoy building innovative solutions and continuously expanding my knowledge. I enjoy building innovative solutions and continuously expanding my knowledge.
</p> </p>
<p className="mt-4 text-gray-700 text-base"> <p className="mt-4 text-gray-700 text-base">
Currently working on exciting projects that merge creativity with functionality. Currently working on exciting projects that merge creativity with functionality.
Always eager to learn and collaborate! Always eager to learn and collaborate!
</p> </p>
</div> </div>
{/* Right Section: Image */}
<div className="flex mt-8 md:mt-0 md:ml-12"> <div className="flex mt-8 md:mt-0 md:ml-12">
<Image <Image
src="/images/me.jpg" src="/images/me.jpg"
@@ -41,4 +45,4 @@ export default function Hero() {
</div> </div>
</div> </div>
); );
} }

View File

@@ -1,8 +1,5 @@
// app/components/Projects.tsx import React, {useEffect, useState} from "react";
"use client";
import Link from "next/link"; import Link from "next/link";
import {useEffect, useState} from "react";
interface Project { interface Project {
id: string; id: string;
@@ -13,37 +10,42 @@ interface Project {
export default function Projects() { export default function Projects() {
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => { useEffect(() => {
const fetchProjects = async () => { const fetchProjects = async () => {
try { try {
const response = await fetch('/api/projects'); 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(); const projectsData = await response.json();
setProjects(projectsData); setProjects(projectsData);
setTimeout(() => {
setIsVisible(true);
}, 250); // Delay to start the animation after Hero
} catch (error) { } catch (error) {
console.error("Failed to fetch projects:", error); console.error("Failed to fetch projects:", error);
} }
}; };
fetchProjects().then(r => r);
fetchProjects();
}, []); }, []);
const numberOfProjects = projects.length;
return ( 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"> <h2 className="text-3xl font-bold text-center text-gray-800">
Projects Projects
</h2> </h2>
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
{projects.map((project) => ( {projects.map((project, index) => (
<Link <Link key={project.id} href={`/Projects/${project.slug}`} className="cursor-pointer">
key={project.id} <div className={`p-4 border shadow-lg bg-white/45 rounded-2xl animate-fly-in`}
href={`/Projects/${project.slug}`} style={{animationDelay: `${index * 0.1}s`}}>
className="cursor-pointer"
>
<div
key={project.id}
className="p-4 border shadow-lg bg-white/45 rounded-2xl"
>
<h3 className="text-2xl font-bold text-gray-800"> <h3 className="text-2xl font-bold text-gray-800">
{project.title} {project.title}
</h3> </h3>
@@ -53,6 +55,15 @@ export default function Projects() {
</div> </div>
</Link> </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> </div>
</section> </section>
); );

View File

@@ -118,4 +118,69 @@ body {
.flex-grow { .flex-grow {
flex-grow: 1; 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 // 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 "./globals.css";
import {Roboto} from 'next/font/google' import {Roboto} from 'next/font/google';
import React from "react"; import React, {useEffect, useState} from "react";
import ClientCookieConsentBanner from "./components/ClientCookieConsentBanner";
const roboto = Roboto({ const roboto = Roboto({
variable: '--font-roboto', variable: '--font-roboto',
weight: '400', weight: '400',
subsets: ['latin'], subsets: ['latin'],
}) });
export const metadata: Metadata = {
title: "Dennis",
description: "A portfolio website showcasing my work and skills.",
};
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode; 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 ( return (
<html lang="en"> <html lang="en">
<body className={roboto.variable}> <body className={roboto.variable}>
<ClientCookieConsentBanner onConsentChange={handleConsentChange}/>
{children} {children}
{consent === "accepted" && <SpeedInsights/>}
{consent === "accepted" && <Analytics/>}
</body> </body>
</html> </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"; "use client";
import Header from "./components/Header"; import Header from "./components/Header";
@@ -5,10 +6,34 @@ import Hero from "./components/Hero";
import Projects from "./components/Projects"; import Projects from "./components/Projects";
import Contact from "./components/Contact"; import Contact from "./components/Contact";
import Footer from "./components/Footer"; import Footer from "./components/Footer";
import Script from "next/script";
export default function Home() { export default function Home() {
return ( return (
<div className="min-h-screen flex flex-col bg-radiant-animated"> <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/> <Header/>
<div className="h-10"></div> <div className="h-10"></div>
<main> <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", "version": "0.1.0",
"dependencies": { "dependencies": {
"@prisma/client": "^6.1.0", "@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", "gray-matter": "^4.0.3",
"next": "15.1.3", "next": "15.1.3",
"prisma": "^6.1.0", "prisma": "^6.1.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-cookie-consent": "^9.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-markdown": "^9.0.3", "react-markdown": "^9.0.3",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
@@ -949,6 +953,15 @@
"@prisma/debug": "6.3.0" "@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": { "node_modules/@rtsao/scc": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -963,6 +976,22 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@swc/counter": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -1317,6 +1346,93 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC" "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": { "node_modules/acorn": {
"version": "8.14.0", "version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@@ -1658,6 +1774,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -1776,6 +1901,15 @@
"node": ">= 6" "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": { "node_modules/caniuse-lite": {
"version": "1.0.30001696", "version": "1.0.30001696",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz",
@@ -1938,7 +2072,6 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/color-string": { "node_modules/color-string": {
@@ -1994,6 +2127,47 @@
"node": ">= 8" "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": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -2432,6 +2606,12 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/escape-string-regexp": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -3003,6 +3183,12 @@
"reusify": "^1.0.4" "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": { "node_modules/file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -3607,6 +3793,18 @@
"url": "https://opencollective.com/unified" "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": { "node_modules/html-url-attributes": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@@ -4226,6 +4424,12 @@
"jiti": "bin/jiti.js" "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": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4362,6 +4566,16 @@
"url": "https://github.com/sponsors/antonk52" "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": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -5686,6 +5900,12 @@
"dev": true, "dev": true,
"license": "BlueOak-1.0.0" "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": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -5699,6 +5919,16 @@
"node": ">=6" "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": { "node_modules/parse-entities": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
@@ -5976,7 +6206,6 @@
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
@@ -6078,6 +6307,21 @@
"node": ">=0.10.0" "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": { "node_modules/react-dom": {
"version": "19.0.0", "version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
@@ -6402,6 +6646,34 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/scheduler": {
"version": "0.25.0", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
@@ -6749,6 +7021,12 @@
"node": ">=8" "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": { "node_modules/string.prototype.includes": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@@ -7130,6 +7408,12 @@
"node": ">=0.8" "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": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -7333,6 +7617,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/unified": {
"version": "11.0.5", "version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
@@ -7724,6 +8018,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/zwitch": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",

View File

@@ -10,14 +10,18 @@
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^6.1.0", "@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", "next": "15.1.3",
"prisma": "^6.1.0", "prisma": "^6.1.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-markdown": "^9.0.3", "react-markdown": "^9.0.3",
"gray-matter": "^4.0.3", "rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0"
"rehype-raw": "^7.0.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@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