From 36e44ef1b8066443473a914f8d4beb6664151d24 Mon Sep 17 00:00:00 2001 From: Denshooter Date: Tue, 4 Feb 2025 16:44:49 +0100 Subject: [PATCH] feat: add cookie consent banner and privacy policy page; update dependencies and improve animations --- app/Projects/[slug]/Footer.tsx | 32 -- app/Projects/[slug]/page.tsx | 20 +- app/api/og/route.tsx | 76 +++++ app/api/projects/route.tsx | 40 ++- app/components/ClientCookieConsentBanner.tsx | 13 + app/components/Contact.tsx | 50 +-- app/components/CookieConsentBanner.tsx | 98 ++++++ app/components/Footer.tsx | 68 ++-- app/components/Footer_Back.tsx | 50 +++ app/components/Header.tsx | 13 +- app/components/Hero.tsx | 24 +- app/components/Projects.tsx | 45 ++- app/globals.css | 65 ++++ app/layout.tsx | 33 +- app/metadata.tsx | 31 ++ app/page.tsx | 25 ++ app/privacy-policy/page.tsx | 66 ++++ app/sitemap.tsx | 31 ++ package-lock.json | 304 +++++++++++++++++- package.json | 10 +- public/images/me_cutout.png | Bin 1516414 -> 0 bytes public/images/off_white_bg.jpg | Bin 3853881 -> 0 bytes .../{ => private}/project-marie-dennis.md | 0 public/robots.txt | 3 + 24 files changed, 929 insertions(+), 168 deletions(-) delete mode 100644 app/Projects/[slug]/Footer.tsx create mode 100644 app/api/og/route.tsx create mode 100644 app/components/ClientCookieConsentBanner.tsx create mode 100644 app/components/CookieConsentBanner.tsx create mode 100644 app/components/Footer_Back.tsx create mode 100644 app/metadata.tsx create mode 100644 app/privacy-policy/page.tsx create mode 100644 app/sitemap.tsx delete mode 100644 public/images/me_cutout.png delete mode 100644 public/images/off_white_bg.jpg rename public/projects/{ => private}/project-marie-dennis.md (100%) create mode 100644 public/robots.txt diff --git a/app/Projects/[slug]/Footer.tsx b/app/Projects/[slug]/Footer.tsx deleted file mode 100644 index cec5312..0000000 --- a/app/Projects/[slug]/Footer.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import Link from "next/link"; - -export default function Footer() { - return ( - - ); -} \ No newline at end of file diff --git a/app/Projects/[slug]/page.tsx b/app/Projects/[slug]/page.tsx index 8bfabda..500e1e5 100644 --- a/app/Projects/[slug]/page.tsx +++ b/app/Projects/[slug]/page.tsx @@ -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(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 ( -
+
-
- -
-
+
); } \ No newline at end of file diff --git a/app/api/og/route.tsx b/app/api/og/route.tsx new file mode 100644 index 0000000..46ddbc5 --- /dev/null +++ b/app/api/og/route.tsx @@ -0,0 +1,76 @@ +import {ImageResponse} from 'next/og'; + +export async function GET() { + return new ImageResponse( + ( +
+
+ Dennis Konkol | Portfolio +
+
+

Hi, I’m Dennis

+

Student & Software Engineer

+

+ Based in Osnabrück, Germany +

+

+ Passionate about technology, coding, and solving real-world problems. +

+
+ Image of Dennis +
+ ), + { + width: 1200, + height: 630, + } + ); +} \ No newline at end of file diff --git a/app/api/projects/route.tsx b/app/api/projects/route.tsx index f60c017..9f12fef 100644 --- a/app/api/projects/route.tsx +++ b/app/api/projects/route.tsx @@ -4,23 +4,31 @@ import path from 'path'; import matter from 'gray-matter'; export async function GET() { - const projectsDirectory = path.join(process.cwd(), 'public/projects'); - const filenames = fs.readdirSync(projectsDirectory); + try { + 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) => { - const filePath = path.join(projectsDirectory, filename); - const fileContents = fs.readFileSync(filePath, 'utf8'); - const {data} = matter(fileContents); + return { + id: data.id, + title: data.title, + description: data.description, + slug: filename.replace('.md', ''), + }; + }); - return { - id: data.id, - title: data.title, - description: data.description, - slug: filename.replace('.md', ''), - }; - }); - - return NextResponse.json(projects); + return NextResponse.json(projects); + } catch (error) { + console.error("Failed to fetch projects:", error); + return NextResponse.json({error: 'Failed to fetch projects'}, {status: 500}); + } } \ No newline at end of file diff --git a/app/components/ClientCookieConsentBanner.tsx b/app/components/ClientCookieConsentBanner.tsx new file mode 100644 index 0000000..0fc3f04 --- /dev/null +++ b/app/components/ClientCookieConsentBanner.tsx @@ -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 ; +}; + +export default ClientCookieConsentBanner; \ No newline at end of file diff --git a/app/components/Contact.tsx b/app/components/Contact.tsx index af7242e..0c69d48 100644 --- a/app/components/Contact.tsx +++ b/app/components/Contact.tsx @@ -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, - ) => { - 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 ( -
+

Contact Me

-
- {success && ( -

- Your message has been sent successfully! -

- )} - {error &&

{error}

} + +
+
+
+ Privacy + Policy +
+

© Dennis Konkol 2025

+
-

© Dennis Konkol 2025

- ); } \ No newline at end of file diff --git a/app/components/Footer_Back.tsx b/app/components/Footer_Back.tsx new file mode 100644 index 0000000..27f85d5 --- /dev/null +++ b/app/components/Footer_Back.tsx @@ -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 ( +
+
+
+

Connect with me on social platforms:

+
+ + + + + + + + + + +
+
+
+ + Back to main page + +
+
+
+ Privacy + Policy +
+

© Dennis Konkol 2025

+
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 06b6c5d..6bbe7a5 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -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 ( -
+
diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index 0aff926..cee1885 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -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 (
- - {/* Left Section: Text */} -
+ 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'}`}> +

Hi, I’m Dennis

@@ -17,19 +25,15 @@ export default function Hero() {

Based in Osnabrück, Germany

-

Passionate about technology, coding, and solving real-world problems. I enjoy building innovative solutions and continuously expanding my knowledge.

-

Currently working on exciting projects that merge creativity with functionality. Always eager to learn and collaborate!

- - {/* Right Section: Image */}
); -} +} \ No newline at end of file diff --git a/app/components/Projects.tsx b/app/components/Projects.tsx index 1a1c47c..2233f12 100644 --- a/app/components/Projects.tsx +++ b/app/components/Projects.tsx @@ -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([]); + 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 ( -
+

Projects

- {projects.map((project) => ( - -
+ {projects.map((project, index) => ( + +

{project.title}

@@ -53,6 +55,15 @@ export default function Projects() {
))} +
+

+ More to come +

+

+ ... +

+
); diff --git a/app/globals.css b/app/globals.css index 2935243..f7e9beb 100644 --- a/app/globals.css +++ b/app/globals.css @@ -118,4 +118,69 @@ 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; } \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 5f5b4ac..9652c80 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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(null); + + useEffect(() => { + const storedConsent = localStorage.getItem("CookieConsent"); + setConsent(storedConsent); + }, []); + + const handleConsentChange = (newConsent: string) => { + setConsent(newConsent); + }; + return ( + {children} + {consent === "accepted" && } + {consent === "accepted" && } ); diff --git a/app/metadata.tsx b/app/metadata.tsx new file mode 100644 index 0000000..013349b --- /dev/null +++ b/app/metadata.tsx @@ -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"], + }, +}; diff --git a/app/page.tsx b/app/page.tsx index 316eb26..0d44925 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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 ( +
+