Add Drupal headless stack with Next.js frontend
- 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
This commit is contained in:
parent
71a8dac389
commit
f8b8f53d54
85 changed files with 7802 additions and 17 deletions
196
drupal/nextjs/components/home-journey-background.tsx
Normal file
196
drupal/nextjs/components/home-journey-background.tsx
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
"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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue