Add i18n to home sections, improve consent management and middleware asset handling
Co-authored-by: dennis <dennis@konkol.net>
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
import { motion, Variants } from "framer-motion";
|
import { motion, Variants } from "framer-motion";
|
||||||
import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb } from "lucide-react";
|
import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useLocale } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import type { JSONContent } from "@tiptap/react";
|
import type { JSONContent } from "@tiptap/react";
|
||||||
import RichTextClient from "./RichTextClient";
|
import RichTextClient from "./RichTextClient";
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ const fadeInUp: Variants = {
|
|||||||
|
|
||||||
const About = () => {
|
const About = () => {
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
const t = useTranslations("home.about");
|
||||||
const [cmsDoc, setCmsDoc] = useState<JSONContent | null>(null);
|
const [cmsDoc, setCmsDoc] = useState<JSONContent | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -99,7 +100,7 @@ const About = () => {
|
|||||||
variants={fadeInUp}
|
variants={fadeInUp}
|
||||||
className="text-4xl md:text-5xl font-bold text-stone-900"
|
className="text-4xl md:text-5xl font-bold text-stone-900"
|
||||||
>
|
>
|
||||||
About Me
|
{t("title")}
|
||||||
</motion.h2>
|
</motion.h2>
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={fadeInUp}
|
variants={fadeInUp}
|
||||||
@@ -109,26 +110,9 @@ const About = () => {
|
|||||||
<RichTextClient doc={cmsDoc} className="prose prose-stone max-w-none" />
|
<RichTextClient doc={cmsDoc} className="prose prose-stone max-w-none" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p>
|
<p>{t("p1")}</p>
|
||||||
Hi, I'm Dennis – a student and passionate self-hoster based
|
<p>{t("p2")}</p>
|
||||||
in Osnabrück, Germany.
|
<p>{t("p3")}</p>
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
I love building full-stack web applications with{" "}
|
|
||||||
<strong>Next.js</strong> and mobile apps with{" "}
|
|
||||||
<strong>Flutter</strong>. But what really excites me is{" "}
|
|
||||||
<strong>DevOps</strong>: I run my own infrastructure on{" "}
|
|
||||||
<strong>IONOS</strong> and <strong>OVHcloud</strong>, managing
|
|
||||||
everything with <strong>Docker Swarm</strong>,{" "}
|
|
||||||
<strong>Traefik</strong>, and automated CI/CD pipelines with my
|
|
||||||
own runners.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
When I'm not coding or tinkering with servers, you'll
|
|
||||||
find me <strong>gaming</strong>, <strong>jogging</strong>, or
|
|
||||||
experimenting with new tech like game servers or automation
|
|
||||||
workflows with <strong>n8n</strong>.
|
|
||||||
</p>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -139,12 +123,10 @@ const About = () => {
|
|||||||
<Lightbulb size={20} className="text-stone-600 flex-shrink-0 mt-0.5" />
|
<Lightbulb size={20} className="text-stone-600 flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-stone-800 mb-1">
|
<p className="text-sm font-semibold text-stone-800 mb-1">
|
||||||
Fun Fact
|
{t("funFactTitle")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-stone-700 leading-relaxed">
|
<p className="text-sm text-stone-700 leading-relaxed">
|
||||||
Even though I automate a lot, I still use pen and paper
|
{t("funFactBody")}
|
||||||
for my calendar and notes – it helps me clear my head and
|
|
||||||
stay focused.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1457,7 +1457,26 @@ export default function ActivityFeed() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Don't render if tracking is disabled and no data
|
// Don't render if tracking is disabled and no data
|
||||||
if (!isTrackingEnabled && !data) return null;
|
if (!isTrackingEnabled && !data) {
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-4 right-4 md:bottom-6 md:right-6 z-40 pointer-events-auto">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
className="bg-black/80 backdrop-blur-xl border border-white/10 rounded-xl p-3 shadow-2xl"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={toggleTracking}
|
||||||
|
className="flex items-center gap-2 text-white/60 hover:text-white transition-colors"
|
||||||
|
title="Activity tracking is disabled. Click to enable."
|
||||||
|
>
|
||||||
|
<Activity size={16} />
|
||||||
|
<span className="text-xs">Tracking disabled</span>
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// If tracking disabled but we have data, show a disabled state
|
// If tracking disabled but we have data, show a disabled state
|
||||||
if (!isTrackingEnabled && data) {
|
if (!isTrackingEnabled && data) {
|
||||||
|
|||||||
@@ -38,9 +38,11 @@ function writeConsentCookie(value: ConsentState) {
|
|||||||
const ConsentContext = createContext<{
|
const ConsentContext = createContext<{
|
||||||
consent: ConsentState | null;
|
consent: ConsentState | null;
|
||||||
setConsent: (next: ConsentState) => void;
|
setConsent: (next: ConsentState) => void;
|
||||||
|
resetConsent: () => void;
|
||||||
}>({
|
}>({
|
||||||
consent: null,
|
consent: null,
|
||||||
setConsent: () => {},
|
setConsent: () => {},
|
||||||
|
resetConsent: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ConsentProvider({ children }: { children: React.ReactNode }) {
|
export function ConsentProvider({ children }: { children: React.ReactNode }) {
|
||||||
@@ -55,7 +57,16 @@ export function ConsentProvider({ children }: { children: React.ReactNode }) {
|
|||||||
writeConsentCookie(next);
|
writeConsentCookie(next);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const value = useMemo(() => ({ consent, setConsent }), [consent, setConsent]);
|
const resetConsent = useCallback(() => {
|
||||||
|
setConsentState(null);
|
||||||
|
// expire cookie
|
||||||
|
document.cookie = `${COOKIE_NAME}=; path=/; max-age=0; samesite=lax`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({ consent, setConsent, resetConsent }),
|
||||||
|
[consent, setConsent, resetConsent],
|
||||||
|
);
|
||||||
|
|
||||||
return <ConsentContext.Provider value={value}>{children}</ConsentContext.Provider>;
|
return <ConsentContext.Provider value={value}>{children}</ConsentContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,16 @@ import { useState, useEffect } from "react";
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { Mail, MapPin, Send } from "lucide-react";
|
import { Mail, MapPin, Send } from "lucide-react";
|
||||||
import { useToast } from "@/components/Toast";
|
import { useToast } from "@/components/Toast";
|
||||||
import { useLocale } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import type { JSONContent } from "@tiptap/react";
|
import type { JSONContent } from "@tiptap/react";
|
||||||
import RichTextClient from "./RichTextClient";
|
import RichTextClient from "./RichTextClient";
|
||||||
|
|
||||||
const Contact = () => {
|
const Contact = () => {
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
const { showEmailSent, showEmailError } = useToast();
|
const { showEmailSent, showEmailError } = useToast();
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
const t = useTranslations("home.contact");
|
||||||
const [cmsDoc, setCmsDoc] = useState<JSONContent | null>(null);
|
const [cmsDoc, setCmsDoc] = useState<JSONContent | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -164,10 +160,6 @@ const Contact = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!mounted) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id="contact"
|
id="contact"
|
||||||
@@ -183,14 +175,13 @@ const Contact = () => {
|
|||||||
className="text-center mb-16"
|
className="text-center mb-16"
|
||||||
>
|
>
|
||||||
<h2 className="text-4xl md:text-5xl font-bold mb-6 text-stone-900">
|
<h2 className="text-4xl md:text-5xl font-bold mb-6 text-stone-900">
|
||||||
Contact Me
|
{t("title")}
|
||||||
</h2>
|
</h2>
|
||||||
{cmsDoc ? (
|
{cmsDoc ? (
|
||||||
<RichTextClient doc={cmsDoc} className="prose prose-stone max-w-2xl mx-auto mt-4 text-stone-700" />
|
<RichTextClient doc={cmsDoc} className="prose prose-stone max-w-2xl mx-auto mt-4 text-stone-700" />
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xl text-stone-700 max-w-2xl mx-auto mt-4">
|
<p className="text-xl text-stone-700 max-w-2xl mx-auto mt-4">
|
||||||
Interested in working together or have questions about my projects?
|
{t("subtitle")}
|
||||||
Feel free to reach out!
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -206,12 +197,10 @@ const Contact = () => {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl font-bold text-stone-900 mb-6">
|
<h3 className="text-2xl font-bold text-stone-900 mb-6">
|
||||||
Get In Touch
|
{t("getInTouch")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-stone-700 leading-relaxed">
|
<p className="text-stone-700 leading-relaxed">
|
||||||
I'm always available to discuss new opportunities,
|
{t("getInTouchBody")}
|
||||||
interesting projects, or simply chat about technology and
|
|
||||||
innovation.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ import { Heart, Code } from 'lucide-react';
|
|||||||
import { SiGithub, SiLinkedin } from 'react-icons/si';
|
import { SiGithub, SiLinkedin } from 'react-icons/si';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useLocale } from "next-intl";
|
import { useLocale } from "next-intl";
|
||||||
|
import { useConsent } from "./ConsentProvider";
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
const [currentYear, setCurrentYear] = useState(2024);
|
const [currentYear, setCurrentYear] = useState(2024);
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
const { resetConsent } = useConsent();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentYear(new Date().getFullYear());
|
setCurrentYear(new Date().getFullYear());
|
||||||
setMounted(true);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const socialLinks = [
|
const socialLinks = [
|
||||||
@@ -22,10 +22,6 @@ const Footer = () => {
|
|||||||
{ icon: SiLinkedin, href: 'https://linkedin.com/in/dkonkol', label: 'LinkedIn' }
|
{ icon: SiLinkedin, href: 'https://linkedin.com/in/dkonkol', label: 'LinkedIn' }
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!mounted) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="relative py-12 px-4 bg-white border-t border-stone-200">
|
<footer className="relative py-12 px-4 bg-white border-t border-stone-200">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
@@ -117,6 +113,14 @@ const Footer = () => {
|
|||||||
>
|
>
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</Link>
|
</Link>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => resetConsent()}
|
||||||
|
className="text-stone-500 hover:text-stone-800 transition-colors duration-200"
|
||||||
|
title="Show privacy settings banner again"
|
||||||
|
>
|
||||||
|
Privacy settings
|
||||||
|
</button>
|
||||||
<Link
|
<Link
|
||||||
href="/404"
|
href="/404"
|
||||||
className="text-stone-500 hover:text-stone-800 transition-colors duration-200 font-mono text-xs"
|
className="text-stone-500 hover:text-stone-800 transition-colors duration-200 font-mono text-xs"
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { usePathname, useRouter } from "next/navigation";
|
|||||||
const Header = () => {
|
const Header = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false);
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -19,13 +18,6 @@ const Header = () => {
|
|||||||
|
|
||||||
const isHome = pathname === `/${locale}` || pathname === `/${locale}/`;
|
const isHome = pathname === `/${locale}` || pathname === `/${locale}/`;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Use requestAnimationFrame to ensure smooth transition
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
setMounted(true);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
setScrolled(window.scrollY > 50);
|
setScrolled(window.scrollY > 50);
|
||||||
@@ -69,10 +61,9 @@ const Header = () => {
|
|||||||
<>
|
<>
|
||||||
<motion.header
|
<motion.header
|
||||||
initial={false}
|
initial={false}
|
||||||
animate={{ y: 0, opacity: mounted ? 1 : 0 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
className="fixed top-6 left-0 right-0 z-50 flex justify-center px-4 pointer-events-none"
|
className="fixed top-6 left-0 right-0 z-50 flex justify-center px-4 pointer-events-none"
|
||||||
style={{ opacity: mounted ? 1 : 0 }}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`pointer-events-auto transition-all duration-500 ease-out ${
|
className={`pointer-events-auto transition-all duration-500 ease-out ${
|
||||||
@@ -81,7 +72,7 @@ const Header = () => {
|
|||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={false}
|
initial={false}
|
||||||
animate={{ opacity: mounted ? 1 : 0, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
className={`
|
className={`
|
||||||
backdrop-blur-xl transition-all duration-500
|
backdrop-blur-xl transition-all duration-500
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import { motion } from "framer-motion";
|
|||||||
import { ArrowDown, Code, Zap, Rocket } from "lucide-react";
|
import { ArrowDown, Code, Zap, Rocket } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useLocale } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import type { JSONContent } from "@tiptap/react";
|
import type { JSONContent } from "@tiptap/react";
|
||||||
import RichTextClient from "./RichTextClient";
|
import RichTextClient from "./RichTextClient";
|
||||||
|
|
||||||
const Hero = () => {
|
const Hero = () => {
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
const t = useTranslations("home.hero");
|
||||||
const [cmsDoc, setCmsDoc] = useState<JSONContent | null>(null);
|
const [cmsDoc, setCmsDoc] = useState<JSONContent | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -29,9 +30,9 @@ const Hero = () => {
|
|||||||
}, [locale]);
|
}, [locale]);
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
{ icon: Code, text: "Next.js & Flutter" },
|
{ icon: Code, text: t("features.f1") },
|
||||||
{ icon: Zap, text: "Docker Swarm & CI/CD" },
|
{ icon: Zap, text: t("features.f2") },
|
||||||
{ icon: Rocket, text: "Self-Hosted Infrastructure" },
|
{ icon: Rocket, text: t("features.f3") },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -178,21 +179,7 @@ const Hero = () => {
|
|||||||
{cmsDoc ? (
|
{cmsDoc ? (
|
||||||
<RichTextClient doc={cmsDoc} className="prose prose-stone max-w-none" />
|
<RichTextClient doc={cmsDoc} className="prose prose-stone max-w-none" />
|
||||||
) : (
|
) : (
|
||||||
<p>
|
<p>{t("description")}</p>
|
||||||
Student and passionate{" "}
|
|
||||||
<span className="text-stone-900 font-semibold decoration-liquid-mint decoration-2 underline underline-offset-4">
|
|
||||||
self-hoster
|
|
||||||
</span>{" "}
|
|
||||||
building full-stack web apps and mobile solutions. I run my own{" "}
|
|
||||||
<span className="text-stone-900 font-semibold decoration-liquid-lavender decoration-2 underline underline-offset-4">
|
|
||||||
infrastructure
|
|
||||||
</span>{" "}
|
|
||||||
and love exploring{" "}
|
|
||||||
<span className="text-stone-900 font-semibold decoration-liquid-rose decoration-2 underline underline-offset-4">
|
|
||||||
DevOps
|
|
||||||
</span>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -238,7 +225,7 @@ const Hero = () => {
|
|||||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
className="px-8 py-4 bg-stone-900 text-cream rounded-full shadow-lg hover:shadow-xl hover:bg-stone-950 transition-all duration-500 flex items-center gap-2"
|
className="px-8 py-4 bg-stone-900 text-cream rounded-full shadow-lg hover:shadow-xl hover:bg-stone-950 transition-all duration-500 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<span className="text-cream">View My Work</span>
|
<span className="text-cream">{t("ctaWork")}</span>
|
||||||
<ArrowDown size={18} />
|
<ArrowDown size={18} />
|
||||||
</motion.a>
|
</motion.a>
|
||||||
|
|
||||||
@@ -249,7 +236,7 @@ const Hero = () => {
|
|||||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
className="px-8 py-4 bg-white text-stone-900 border border-stone-200 rounded-full font-medium shadow-sm hover:shadow-md transition-all duration-500"
|
className="px-8 py-4 bg-white text-stone-900 border border-stone-200 rounded-full font-medium shadow-sm hover:shadow-md transition-all duration-500"
|
||||||
>
|
>
|
||||||
<span>Contact Me</span>
|
<span>{t("ctaContact")}</span>
|
||||||
</motion.a>
|
</motion.a>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,6 +48,41 @@ jest.mock("next-intl", () => ({
|
|||||||
};
|
};
|
||||||
return map[key] || key;
|
return map[key] || key;
|
||||||
}
|
}
|
||||||
|
if (namespace === "home.hero") {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
"features.f1": "Next.js & Flutter",
|
||||||
|
"features.f2": "Docker Swarm & CI/CD",
|
||||||
|
"features.f3": "Self-Hosted Infrastructure",
|
||||||
|
description:
|
||||||
|
"Student and passionate self-hoster building full-stack web apps and mobile solutions. I run my own infrastructure and love exploring DevOps.",
|
||||||
|
ctaWork: "View My Work",
|
||||||
|
ctaContact: "Contact Me",
|
||||||
|
};
|
||||||
|
return map[key] || key;
|
||||||
|
}
|
||||||
|
if (namespace === "home.about") {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
title: "About Me",
|
||||||
|
p1: "Hi, I'm Dennis – a student and passionate self-hoster based in Osnabrück, Germany.",
|
||||||
|
p2: "I love building full-stack web applications with Next.js and mobile apps with Flutter. But what really excites me is DevOps: I run my own infrastructure and automate deployments with CI/CD.",
|
||||||
|
p3: "When I'm not coding or tinkering with servers, you'll find me gaming, jogging, or experimenting with automation workflows.",
|
||||||
|
funFactTitle: "Fun Fact",
|
||||||
|
funFactBody:
|
||||||
|
"Even though I automate a lot, I still use pen and paper for my calendar and notes – it helps me stay focused.",
|
||||||
|
};
|
||||||
|
return map[key] || key;
|
||||||
|
}
|
||||||
|
if (namespace === "home.contact") {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
title: "Contact Me",
|
||||||
|
subtitle:
|
||||||
|
"Interested in working together or have questions about my projects? Feel free to reach out!",
|
||||||
|
getInTouch: "Get In Touch",
|
||||||
|
getInTouchBody:
|
||||||
|
"I'm always available to discuss new opportunities, interesting projects, or simply chat about technology and innovation.",
|
||||||
|
};
|
||||||
|
return map[key] || key;
|
||||||
|
}
|
||||||
return key;
|
return key;
|
||||||
},
|
},
|
||||||
NextIntlClientProvider: ({ children }: { children: React.ReactNode }) =>
|
NextIntlClientProvider: ({ children }: { children: React.ReactNode }) =>
|
||||||
|
|||||||
@@ -21,5 +21,32 @@
|
|||||||
"acceptSelected": "Auswahl akzeptieren",
|
"acceptSelected": "Auswahl akzeptieren",
|
||||||
"rejectAll": "Alles ablehnen"
|
"rejectAll": "Alles ablehnen"
|
||||||
}
|
}
|
||||||
|
,
|
||||||
|
"home": {
|
||||||
|
"hero": {
|
||||||
|
"features": {
|
||||||
|
"f1": "Next.js & Flutter",
|
||||||
|
"f2": "Docker Swarm & CI/CD",
|
||||||
|
"f3": "Self-Hosted Infrastruktur"
|
||||||
|
},
|
||||||
|
"description": "Student und leidenschaftlicher Self-Hoster: Ich baue Full-Stack Web-Apps und Mobile-Lösungen, betreibe meine eigene Infrastruktur und liebe DevOps.",
|
||||||
|
"ctaWork": "Meine Projekte",
|
||||||
|
"ctaContact": "Kontakt"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "Über mich",
|
||||||
|
"p1": "Hi, ich bin Dennis – Student und leidenschaftlicher Self-Hoster aus Osnabrück.",
|
||||||
|
"p2": "Ich entwickle Full-Stack Web-Apps mit Next.js und Mobile-Apps mit Flutter. Besonders spannend finde ich DevOps: eigene Infrastruktur, Automatisierung und CI/CD Deployments.",
|
||||||
|
"p3": "Wenn ich nicht code oder an Servern schraube, findest du mich beim Gaming, Joggen oder beim Experimentieren mit Automationen.",
|
||||||
|
"funFactTitle": "Fun Fact",
|
||||||
|
"funFactBody": "Auch wenn ich viel automatisiere, nutze ich für Kalender & Notizen noch Stift und Papier – das hilft mir beim Fokus."
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"title": "Kontakt",
|
||||||
|
"subtitle": "Du willst zusammenarbeiten oder hast Fragen zu meinen Projekten? Schreib mir gerne!",
|
||||||
|
"getInTouch": "Melde dich",
|
||||||
|
"getInTouchBody": "Ich bin immer offen für neue Chancen, spannende Projekte oder einfach einen Tech-Talk."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,5 +21,32 @@
|
|||||||
"acceptSelected": "Accept selected",
|
"acceptSelected": "Accept selected",
|
||||||
"rejectAll": "Reject all"
|
"rejectAll": "Reject all"
|
||||||
}
|
}
|
||||||
|
,
|
||||||
|
"home": {
|
||||||
|
"hero": {
|
||||||
|
"features": {
|
||||||
|
"f1": "Next.js & Flutter",
|
||||||
|
"f2": "Docker Swarm & CI/CD",
|
||||||
|
"f3": "Self-Hosted Infrastructure"
|
||||||
|
},
|
||||||
|
"description": "Student and passionate self-hoster building full-stack web apps and mobile solutions. I run my own infrastructure and love exploring DevOps.",
|
||||||
|
"ctaWork": "View My Work",
|
||||||
|
"ctaContact": "Contact Me"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "About Me",
|
||||||
|
"p1": "Hi, I'm Dennis – a student and passionate self-hoster based in Osnabrück, Germany.",
|
||||||
|
"p2": "I love building full-stack web applications with Next.js and mobile apps with Flutter. But what really excites me is DevOps: I run my own infrastructure and automate deployments with CI/CD.",
|
||||||
|
"p3": "When I'm not coding or tinkering with servers, you'll find me gaming, jogging, or experimenting with automation workflows.",
|
||||||
|
"funFactTitle": "Fun Fact",
|
||||||
|
"funFactBody": "Even though I automate a lot, I still use pen and paper for my calendar and notes – it helps me stay focused."
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"title": "Contact Me",
|
||||||
|
"subtitle": "Interested in working together or have questions about my projects? Feel free to reach out!",
|
||||||
|
"getInTouch": "Get In Touch",
|
||||||
|
"getInTouchBody": "I'm always available to discuss new opportunities, interesting projects, or simply chat about technology and innovation."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,23 @@ function hasLocalePrefix(pathname: string): boolean {
|
|||||||
export function middleware(request: NextRequest) {
|
export function middleware(request: NextRequest) {
|
||||||
const { pathname, search } = request.nextUrl;
|
const { pathname, search } = request.nextUrl;
|
||||||
|
|
||||||
|
// If a locale-prefixed request hits a public asset path (e.g. /de/images/me.jpg),
|
||||||
|
// redirect to the non-prefixed asset path.
|
||||||
|
if (hasLocalePrefix(pathname)) {
|
||||||
|
const rest = pathname.replace(/^\/(en|de)/, "") || "/";
|
||||||
|
if (rest.includes(".")) {
|
||||||
|
const responseUrl = request.nextUrl.clone();
|
||||||
|
responseUrl.pathname = rest;
|
||||||
|
const res = NextResponse.redirect(responseUrl);
|
||||||
|
return addHeaders(request, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not locale-route public assets (anything with a dot), robots, sitemap, etc.
|
||||||
|
if (pathname.includes(".")) {
|
||||||
|
return addHeaders(request, NextResponse.next());
|
||||||
|
}
|
||||||
|
|
||||||
// Keep admin + APIs unlocalized for simplicity
|
// Keep admin + APIs unlocalized for simplicity
|
||||||
const isAdminOrApi =
|
const isAdminOrApi =
|
||||||
pathname.startsWith("/api/") ||
|
pathname.startsWith("/api/") ||
|
||||||
@@ -107,8 +124,7 @@ export const config = {
|
|||||||
* - _next/static (static files)
|
* - _next/static (static files)
|
||||||
* - _next/image (image optimization files)
|
* - _next/image (image optimization files)
|
||||||
* - favicon.ico (favicon file)
|
* - favicon.ico (favicon file)
|
||||||
* - any path containing a dot (public assets like /images/*.jpg, /robots.txt, /sitemap.xml, etc.)
|
|
||||||
*/
|
*/
|
||||||
"/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)",
|
"/((?!api|_next/static|_next/image|favicon.ico).*)",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user