- Add Next.js frontend service (nextjs) with Dockerfile and source - Update docker-compose.yml: image names, Drupal 11.3.3, nextjs service - Add docker-compose.override.yml.disabled for dev hot-reload - Add install-headless-modules.sh for OAuth/JSON:API module setup - Add README.md with full setup and configuration guide - Update nginx/Dockerfile and nginx.conf.template for cms. subdomain - Update drupal/Dockerfile PHP-FPM build args - Gitignore **/.vscode/ to prevent IDE workspace files from being tracked
196 lines
7.9 KiB
TypeScript
196 lines
7.9 KiB
TypeScript
"use client"
|
|
|
|
import React, { useEffect, useState } from "react"
|
|
import {
|
|
Lightbulb,
|
|
PenTool,
|
|
Code2,
|
|
Rocket,
|
|
Wrench,
|
|
MessageSquare,
|
|
LifeBuoy,
|
|
} from "lucide-react"
|
|
|
|
const STATION_COLORS: Record<
|
|
string,
|
|
{ border: string; bg: string; text: string; label: string }
|
|
> = {
|
|
idea: { border: "border-amber-400", bg: "bg-amber-50", text: "text-amber-600", label: "text-amber-700" },
|
|
concept: { border: "border-emerald-400", bg: "bg-emerald-50", text: "text-emerald-600", label: "text-emerald-700" },
|
|
consulting: { border: "border-sky-400", bg: "bg-sky-50", text: "text-sky-600", label: "text-sky-700" },
|
|
development: { border: "border-fuchsia-400", bg: "bg-fuchsia-100", text: "text-fuchsia-600", label: "text-fuchsia-700" },
|
|
deployment: { border: "border-sky-400", bg: "bg-sky-50", text: "text-sky-600", label: "text-sky-700" },
|
|
maintenance: { border: "border-slate-400", bg: "bg-slate-100", text: "text-slate-600", label: "text-slate-700" },
|
|
support: { border: "border-rose-400", bg: "bg-rose-50", text: "text-rose-600", label: "text-rose-700" },
|
|
}
|
|
|
|
const STATIONS = [
|
|
{ id: "idea", icon: Lightbulb, label: "Idea", lineThreshold: 0 },
|
|
{ id: "concept", icon: PenTool, label: "Concept", lineThreshold: 15 },
|
|
{ id: "deployment", icon: Rocket, label: "Deployment", lineThreshold: 45 },
|
|
{ id: "maintenance", icon: Wrench, label: "Maintenance", lineThreshold: 80 },
|
|
] as const
|
|
|
|
const RIGHT_STATIONS = [
|
|
{ id: "consulting", icon: MessageSquare, label: "Consulting", lineThreshold: 0 },
|
|
{ id: "development", icon: Code2, label: "Development", lineThreshold: 25 },
|
|
{ id: "support", icon: LifeBuoy, label: "Support", lineThreshold: 60 },
|
|
] as const
|
|
|
|
export function HomeJourneyBackground({ children }: { children: React.ReactNode }) {
|
|
const childArray = React.Children.toArray(children)
|
|
const heroContent = childArray[0]
|
|
const mainContent = childArray.slice(1)
|
|
const [visibleStations, setVisibleStations] = useState<Set<string>>(new Set())
|
|
const [visibleRightStations, setVisibleRightStations] = useState<Set<string>>(
|
|
new Set()
|
|
)
|
|
useEffect(() => {
|
|
const handleScroll = () => {
|
|
const y = window.scrollY
|
|
|
|
const docHeight = document.documentElement.scrollHeight - window.innerHeight
|
|
const scrollProgress = docHeight > 0 ? Math.min(y / docHeight, 1) : 0
|
|
const ideaThreshold = 0.02
|
|
const linePercent =
|
|
scrollProgress >= ideaThreshold
|
|
? Math.min(
|
|
100,
|
|
((scrollProgress - ideaThreshold) / (1 - ideaThreshold)) * 100
|
|
)
|
|
: 0
|
|
|
|
const newVisible = new Set<string>()
|
|
if (scrollProgress >= ideaThreshold) {
|
|
newVisible.add("idea")
|
|
}
|
|
STATIONS.forEach((station) => {
|
|
if (station.id !== "idea" && linePercent >= station.lineThreshold) {
|
|
newVisible.add(station.id)
|
|
}
|
|
})
|
|
setVisibleStations(newVisible)
|
|
|
|
const newVisibleRight = new Set<string>()
|
|
if (scrollProgress >= ideaThreshold) {
|
|
newVisibleRight.add("consulting")
|
|
}
|
|
RIGHT_STATIONS.forEach((station) => {
|
|
if (
|
|
station.id !== "consulting" &&
|
|
linePercent >= station.lineThreshold
|
|
) {
|
|
newVisibleRight.add(station.id)
|
|
}
|
|
})
|
|
setVisibleRightStations(newVisibleRight)
|
|
}
|
|
|
|
handleScroll()
|
|
window.addEventListener("scroll", handleScroll, { passive: true })
|
|
return () => window.removeEventListener("scroll", handleScroll)
|
|
}, [])
|
|
|
|
return (
|
|
<div className="journey-layout -mt-6 flex w-full flex-col px-4 lg:px-6">
|
|
<div className="journey-hero w-full">{heroContent}</div>
|
|
<div className="journey-main-row flex w-full items-stretch gap-4 lg:gap-6">
|
|
<aside
|
|
className="journey-sidebar relative sticky top-24 hidden min-h-full shrink-0 flex-[1] pb-8 pr-4 pt-0 lg:block"
|
|
aria-label="Journey timeline"
|
|
>
|
|
<div className="journey-stations relative flex h-full flex-col">
|
|
<div
|
|
className="journey-sidebar-spacer journey-sidebar-spacer-top min-h-0 flex-[1]"
|
|
aria-hidden
|
|
/>
|
|
<div className="journey-stations-list flex min-h-0 flex-[4] flex-col items-center justify-between">
|
|
{STATIONS.map((station) => {
|
|
const isVisible = visibleStations.has(station.id)
|
|
const colors = STATION_COLORS[station.id]
|
|
return (
|
|
<div
|
|
key={station.id}
|
|
className={`journey-station journey-station-${station.id} relative z-10 flex flex-col items-center gap-2`}
|
|
>
|
|
<div
|
|
className={`journey-station-icon flex size-20 shrink-0 items-center justify-center rounded-full border-2 shadow-sm transition-all duration-500 ${
|
|
isVisible
|
|
? `${colors.border} ${colors.bg} opacity-100 scale-100`
|
|
: "border-slate-200 bg-white opacity-0 scale-90"
|
|
}`}
|
|
>
|
|
<station.icon
|
|
className={`size-11 transition-all duration-500 ${
|
|
isVisible ? colors.text : "text-slate-400"
|
|
}`}
|
|
aria-hidden
|
|
/>
|
|
</div>
|
|
<h3
|
|
className={`journey-station-label text-center text-m font-medium transition-all duration-500 ${
|
|
isVisible ? `${colors.label} opacity-100` : "text-slate-500 opacity-0"
|
|
}`}
|
|
>
|
|
{station.label}
|
|
</h3>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
<div className="journey-content main-contents-main min-w-0 flex-[8]">
|
|
{mainContent}
|
|
</div>
|
|
<aside
|
|
className="journey-sidebar journey-sidebar-right relative sticky top-24 hidden min-h-full shrink-0 flex-[1] pb-8 pl-4 pt-0 lg:block"
|
|
aria-label="Consulting and support journey"
|
|
>
|
|
<div className="journey-stations relative flex h-full flex-col">
|
|
<div
|
|
className="journey-sidebar-spacer journey-sidebar-spacer-top min-h-0 flex-[1]"
|
|
aria-hidden
|
|
/>
|
|
<div className="journey-stations-list flex min-h-0 flex-[4] flex-col items-center justify-around">
|
|
{RIGHT_STATIONS.map((station) => {
|
|
const isVisible = visibleRightStations.has(station.id)
|
|
const colors = STATION_COLORS[station.id]
|
|
return (
|
|
<div
|
|
key={station.id}
|
|
className={`journey-station journey-station-${station.id} relative z-10 flex flex-col items-center gap-2`}
|
|
>
|
|
<div
|
|
className={`journey-station-icon flex size-20 shrink-0 items-center justify-center rounded-full border-2 shadow-sm transition-all duration-500 ${
|
|
isVisible
|
|
? `${colors.border} ${colors.bg} opacity-100 scale-100`
|
|
: "border-slate-200 bg-white opacity-0 scale-90"
|
|
}`}
|
|
>
|
|
<station.icon
|
|
className={`size-11 transition-all duration-500 ${
|
|
isVisible ? colors.text : "text-slate-400"
|
|
}`}
|
|
aria-hidden
|
|
/>
|
|
</div>
|
|
<h3
|
|
className={`journey-station-label text-center text-m font-medium transition-all duration-500 ${
|
|
isVisible
|
|
? `${colors.label} opacity-100`
|
|
: "text-slate-500 opacity-0"
|
|
}`}
|
|
>
|
|
{station.label}
|
|
</h3>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|