feat: automate availability and fix KLZ showcase 404s
Some checks failed
Some checks failed
This commit is contained in:
@@ -78,6 +78,28 @@
|
|||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Interactive elements pointer */
|
||||||
|
a,
|
||||||
|
button,
|
||||||
|
[role="button"],
|
||||||
|
input[type="button"],
|
||||||
|
input[type="submit"],
|
||||||
|
input[type="reset"],
|
||||||
|
select,
|
||||||
|
summary,
|
||||||
|
label[for] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure disabled elements don't show pointer */
|
||||||
|
button:disabled,
|
||||||
|
[role="button"]:disabled,
|
||||||
|
input:disabled,
|
||||||
|
select:disabled,
|
||||||
|
button[disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* Remove default tap highlight on mobile */
|
/* Remove default tap highlight on mobile */
|
||||||
* {
|
* {
|
||||||
-webkit-tap-highlight-color: transparent !important;
|
-webkit-tap-highlight-color: transparent !important;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { HeroSection } from "@/src/components/HeroSection";
|
|||||||
import { GlitchText } from "@/src/components/GlitchText";
|
import { GlitchText } from "@/src/components/GlitchText";
|
||||||
import { Marker } from "@/src/components/Marker";
|
import { Marker } from "@/src/components/Marker";
|
||||||
import { PenCircle } from "@/src/components/PenCircle";
|
import { PenCircle } from "@/src/components/PenCircle";
|
||||||
|
import { Availability } from "@/src/components/Availability";
|
||||||
|
|
||||||
export default function LandingPage() {
|
export default function LandingPage() {
|
||||||
return (
|
return (
|
||||||
@@ -313,7 +314,7 @@ export default function LandingPage() {
|
|||||||
</div>
|
</div>
|
||||||
<BodyText className="text-sm md:text-base leading-snug">
|
<BodyText className="text-sm md:text-base leading-snug">
|
||||||
Aktuell nehme ich Projekte für{" "}
|
Aktuell nehme ich Projekte für{" "}
|
||||||
<span className="font-bold text-slate-900">Q2 2026</span>{" "}
|
<span className="font-bold text-slate-900"><Availability /></span>{" "}
|
||||||
an.
|
an.
|
||||||
</BodyText>
|
</BodyText>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
26
apps/web/src/components/Availability.tsx
Normal file
26
apps/web/src/components/Availability.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatisierte Anzeige der Projekt-Verfügbarkeit nach Quartalen.
|
||||||
|
* Berechnet das aktuelle Quartal basierend auf dem heutigen Datum.
|
||||||
|
*/
|
||||||
|
export const Availability: React.FC = () => {
|
||||||
|
const [text, setText] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const now = new Date();
|
||||||
|
const month = now.getMonth(); // 0-11
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const quarter = Math.floor(month / 3) + 1;
|
||||||
|
|
||||||
|
// Aktuelles Quartal formatiert (z.B. Q2 2026)
|
||||||
|
setText(`Q${quarter} ${year}`);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Während Hydrierung nichts rendern, um Mismatches zu vermeiden (bei Client-only Logic)
|
||||||
|
if (!text) return <span className="opacity-0">Q- 202-</span>;
|
||||||
|
|
||||||
|
return <>{text}</>;
|
||||||
|
};
|
||||||
@@ -11,19 +11,32 @@ export function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
|
|
||||||
// 2. Legacy Showcase Asset Mapping
|
// 2. Legacy Showcase Asset Mapping
|
||||||
// Matches: /wp-content/*, /wp-includes/*, /assets/*, and their relative subpaths
|
// Handles:
|
||||||
const showcaseAssetPattern = /^\/(?:wp-content|wp-includes|assets|(?:case-studies|work|blog)\/(?:wp-content|wp-includes|assets))\/(.*)/;
|
// - Global assets (/wp-content/*, /wp-includes/*, /assets/*)
|
||||||
const match = pathname.match(showcaseAssetPattern);
|
// - Showcase-specific resources (/case-studies/klz-cables/index.html, etc.)
|
||||||
|
|
||||||
|
// Map Pattern 1: Global assets (directly under root)
|
||||||
|
if (pathname.match(/^\/(?:wp-content|wp-includes|assets)\//)) {
|
||||||
|
return NextResponse.rewrite(new URL(`/showcase/klz-cables.com${pathname}`, request.url));
|
||||||
|
}
|
||||||
|
|
||||||
if (match) {
|
// Map Pattern 2: Showcase-prefixed assets
|
||||||
const assetPath = match[0];
|
const showcaseMatch = pathname.match(/^\/(?:case-studies|work|blog)\/([^\/]+)\/(.*)/);
|
||||||
|
if (showcaseMatch) {
|
||||||
|
const [, slug, remainingPath] = showcaseMatch;
|
||||||
|
|
||||||
// Normalize path by stripping the case-studies/work/blog prefix if present
|
// Safety: Only rewrite if it looks like a static asset (has extension)
|
||||||
const normalizedPath = assetPath.replace(/^\/(?:case-studies|work|blog)/, '');
|
// or belongs to known legacy folders. This prevents catching Next.js routes.
|
||||||
|
if (remainingPath.match(/\.(?:html|php|js|css|png|jpg|jpeg|svg|gif|woff2?|ttf|pdf|json|xml)$/) ||
|
||||||
// Map to the literal directory in public/
|
remainingPath.includes('wp-content/') ||
|
||||||
// Destination: /showcase/klz-cables.com/...
|
remainingPath.includes('wp-includes/') ||
|
||||||
return NextResponse.rewrite(new URL(`/showcase/klz-cables.com${normalizedPath}`, request.url));
|
remainingPath.includes('assets/')) {
|
||||||
|
|
||||||
|
// Normalize slug: klz-cables -> klz-cables.com (literal folder name)
|
||||||
|
const directory = slug === 'klz-cables' ? 'klz-cables.com' : slug;
|
||||||
|
|
||||||
|
return NextResponse.rewrite(new URL(`/showcase/${directory}/${remainingPath}`, request.url));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
@@ -35,14 +48,8 @@ export const config = {
|
|||||||
'/wp-content/:path*',
|
'/wp-content/:path*',
|
||||||
'/wp-includes/:path*',
|
'/wp-includes/:path*',
|
||||||
'/assets/:path*',
|
'/assets/:path*',
|
||||||
'/case-studies/wp-content/:path*',
|
'/case-studies/:path*',
|
||||||
'/case-studies/wp-includes/:path*',
|
'/work/:path*',
|
||||||
'/case-studies/assets/:path*',
|
'/blog/:path*',
|
||||||
'/work/wp-content/:path*',
|
|
||||||
'/work/wp-includes/:path*',
|
|
||||||
'/work/assets/:path*',
|
|
||||||
'/blog/wp-content/:path*',
|
|
||||||
'/blog/wp-includes/:path*',
|
|
||||||
'/blog/assets/:path*',
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,6 +79,28 @@
|
|||||||
outline: 2px solid #3b82f6;
|
outline: 2px solid #3b82f6;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Interactive elements pointer */
|
||||||
|
a,
|
||||||
|
button,
|
||||||
|
[role="button"],
|
||||||
|
input[type="button"],
|
||||||
|
input[type="submit"],
|
||||||
|
input[type="reset"],
|
||||||
|
select,
|
||||||
|
summary,
|
||||||
|
label[for] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure disabled elements don't show pointer */
|
||||||
|
button:disabled,
|
||||||
|
[role="button"]:disabled,
|
||||||
|
input:disabled,
|
||||||
|
select:disabled,
|
||||||
|
button[disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Components - Tailwind utility classes */
|
/* Components - Tailwind utility classes */
|
||||||
|
|||||||
Reference in New Issue
Block a user