feat(ui): Add Feature3D intro to 3D Viewer
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run

- Added Feature3D AuraUI component with framer-motion animations
- Integrated as a landing overlay on Facility3DViewerPage
This commit is contained in:
fullsizemalt 2025-12-19 17:22:22 -08:00
parent 609e7cb23c
commit d44238417b
2 changed files with 144 additions and 0 deletions

View file

@ -0,0 +1,129 @@
import { useRef } from "react";
import { Link } from "react-router-dom";
import { CheckCircle, ArrowRight, Box, Layers, Zap } from "lucide-react";
import { motion, useScroll, useTransform } from "framer-motion";
interface Feature3DProps {
onEnter?: () => void;
}
const Feature3D = ({ onEnter }: Feature3DProps) => {
const targetRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: targetRef,
offset: ["start end", "end start"],
});
const y = useTransform(scrollYProgress, [0, 1], [100, -100]);
const rotate = useTransform(scrollYProgress, [0, 1], [0, 45]);
return (
<section ref={targetRef} className="py-20 bg-slate-950 overflow-hidden relative min-h-screen flex items-center">
<div className="container px-4 mx-auto relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
{/* Text Content */}
<div>
<motion.div
initial={{ opacity: 0, x: -50 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8 }}
>
<h2 className="text-4xl md:text-5xl font-bold tracking-tight mb-6 bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 to-purple-500">
Digital Twin <br /> Visualization
</h2>
<p className="text-xl text-slate-400 mb-8 leading-relaxed">
Experience your facility like never before. Our real-time 3D engine maps every sensor, plant, and compliance event to its exact physical location.
</p>
<ul className="space-y-4 mb-10">
{[
{ icon: Box, text: "Spatial Inventory Tracking" },
{ icon: Zap, text: "Live Environmental Heatmaps" },
{ icon: Layers, text: "Historical Time-Travel Playback" },
].map((item, index) => (
<motion.li
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.2, duration: 0.5 }}
className="flex items-center text-lg font-medium text-slate-200"
>
<div className="w-10 h-10 rounded-full bg-cyan-500/10 flex items-center justify-center mr-4 text-cyan-400">
<item.icon className="w-5 h-5" />
</div>
{item.text}
</motion.li>
))}
</ul>
{onEnter ? (
<button
onClick={onEnter}
className="inline-flex items-center px-8 py-4 rounded-full bg-cyan-600 text-white font-semibold hover:bg-cyan-500 transition-all shadow-lg shadow-cyan-500/25 border border-cyan-400/20"
>
Enter 3D View <ArrowRight className="ml-2 w-5 h-5" />
</button>
) : (
<Link
to="/facility/3d"
className="inline-flex items-center px-8 py-4 rounded-full bg-cyan-600 text-white font-semibold hover:bg-cyan-500 transition-all shadow-lg shadow-cyan-500/25 border border-cyan-400/20"
>
Enter 3D View <ArrowRight className="ml-2 w-5 h-5" />
</Link>
)}
</motion.div>
</div>
{/* 3D Visuals */}
<div className="relative h-[600px] flex items-center justify-center">
{/* Floating Elements mimicking the AuraUI 3D vibe */}
<motion.div style={{ y, rotate }} className="relative w-full h-full max-w-md aspect-square">
{/* Central Sphere */}
<div className="absolute inset-0 m-auto w-64 h-64 rounded-full bg-gradient-to-tr from-cyan-500 to-blue-600 blur-md opacity-20 animate-pulse" />
<div className="absolute inset-0 m-auto w-60 h-60 rounded-full bg-gradient-to-tr from-cyan-400 to-blue-500 shadow-[0_0_50px_rgba(6,182,212,0.5)] border border-cyan-300/30 backdrop-blur-sm" />
{/* Floating Cubes */}
<motion.div
className="absolute top-10 right-10 w-20 h-20 bg-purple-500/20 rounded-xl border border-purple-400/30 backdrop-blur-md shadow-xl"
animate={{ y: [0, -20, 0], rotate: [0, 10, 0] }}
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
/>
<motion.div
className="absolute bottom-20 left-0 w-24 h-24 bg-lime-500/20 rounded-xl border border-lime-400/30 backdrop-blur-md shadow-xl"
animate={{ y: [0, 30, 0], rotate: [0, -5, 0] }}
transition={{ duration: 5, repeat: Infinity, ease: "easeInOut", delay: 1 }}
/>
{/* Mock UI Card Floating */}
<motion.div
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 bg-card/80 backdrop-blur-xl border border-border p-4 rounded-lg shadow-2xl"
initial={{ scale: 0.8, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.5 }}
>
<div className="flex items-center gap-3 mb-3">
<div className="w-3 h-3 rounded-full bg-red-500" />
<div className="w-3 h-3 rounded-full bg-yellow-500" />
<div className="w-3 h-3 rounded-full bg-green-500" />
</div>
<div className="space-y-2">
<div className="h-2 bg-muted rounded w-3/4" />
<div className="h-2 bg-muted rounded w-1/2" />
<div className="h-20 bg-muted/50 rounded w-full mt-4 flex items-center justify-center text-xs text-muted-foreground">
Real-time Mesh Data
</div>
</div>
</motion.div>
</motion.div>
</div>
</div>
</div>
{/* Background Gradients */}
<div className="absolute top-0 right-0 w-1/3 h-full bg-gradient-to-l from-primary/5 to-transparent pointer-events-none" />
<div className="absolute bottom-0 left-0 w-1/3 h-full bg-gradient-to-r from-purple-500/5 to-transparent pointer-events-none" />
</section>
);
};
export default Feature3D;

View file

@ -15,6 +15,7 @@ import { CameraPreset, CameraPresetSelector } from '../components/facility3d/Cam
import { HierarchyBreadcrumb } from '../components/facility3d/HierarchyBreadcrumb';
import { PlantDataCard } from '../components/facility3d/PlantDataCard';
import { SCALE } from '../components/facility3d/coordinates';
import Feature3D from '../components/aura/Feature3D';
// ... calculatePlantCoords and ErrorBoundary remain unchanged ...
@ -81,6 +82,7 @@ class ErrorBoundary extends Component<{ children: ReactNode }, ErrorBoundaryStat
// --- Main Component ---
export default function Facility3DViewerPage() {
const [showIntro, setShowIntro] = useState(true);
const [status, setStatus] = useState('Initializing...');
const [floorData, setFloorData] = useState<Floor3DData | null>(null);
const [selectedPlant, setSelectedPlant] = useState<PlantPosition | null>(null);
@ -110,6 +112,13 @@ export default function Facility3DViewerPage() {
const [searchParams] = useSearchParams();
const targetedPlantTag = searchParams.get('plant');
useEffect(() => {
// If coming from deep link, skip intro
if (targetedPlantTag) {
setShowIntro(false);
}
}, [targetedPlantTag]);
useEffect(() => {
loadData();
}, []);
@ -295,6 +304,12 @@ export default function Facility3DViewerPage() {
return (
<div className="h-screen w-full relative bg-slate-950 text-white overflow-hidden font-sans">
{/* Intro Overlay */}
{showIntro && (
<div className="absolute inset-0 z-50 bg-slate-950 overflow-y-auto">
<Feature3D onEnter={() => setShowIntro(false)} />
</div>
)}
{/* Header */}
<div className="absolute top-0 left-0 right-0 p-3 md:p-4 z-10 flex flex-wrap items-center justify-between bg-gradient-to-b from-black/90 to-transparent pointer-events-none gap-2">
<div className="pointer-events-auto flex items-center gap-3">