feat(ui): Add Feature3D intro to 3D Viewer
- Added Feature3D AuraUI component with framer-motion animations - Integrated as a landing overlay on Facility3DViewerPage
This commit is contained in:
parent
609e7cb23c
commit
d44238417b
2 changed files with 144 additions and 0 deletions
129
frontend/src/components/aura/Feature3D.tsx
Normal file
129
frontend/src/components/aura/Feature3D.tsx
Normal 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;
|
||||||
|
|
@ -15,6 +15,7 @@ import { CameraPreset, CameraPresetSelector } from '../components/facility3d/Cam
|
||||||
import { HierarchyBreadcrumb } from '../components/facility3d/HierarchyBreadcrumb';
|
import { HierarchyBreadcrumb } from '../components/facility3d/HierarchyBreadcrumb';
|
||||||
import { PlantDataCard } from '../components/facility3d/PlantDataCard';
|
import { PlantDataCard } from '../components/facility3d/PlantDataCard';
|
||||||
import { SCALE } from '../components/facility3d/coordinates';
|
import { SCALE } from '../components/facility3d/coordinates';
|
||||||
|
import Feature3D from '../components/aura/Feature3D';
|
||||||
|
|
||||||
// ... calculatePlantCoords and ErrorBoundary remain unchanged ...
|
// ... calculatePlantCoords and ErrorBoundary remain unchanged ...
|
||||||
|
|
||||||
|
|
@ -81,6 +82,7 @@ class ErrorBoundary extends Component<{ children: ReactNode }, ErrorBoundaryStat
|
||||||
|
|
||||||
// --- Main Component ---
|
// --- Main Component ---
|
||||||
export default function Facility3DViewerPage() {
|
export default function Facility3DViewerPage() {
|
||||||
|
const [showIntro, setShowIntro] = useState(true);
|
||||||
const [status, setStatus] = useState('Initializing...');
|
const [status, setStatus] = useState('Initializing...');
|
||||||
const [floorData, setFloorData] = useState<Floor3DData | null>(null);
|
const [floorData, setFloorData] = useState<Floor3DData | null>(null);
|
||||||
const [selectedPlant, setSelectedPlant] = useState<PlantPosition | null>(null);
|
const [selectedPlant, setSelectedPlant] = useState<PlantPosition | null>(null);
|
||||||
|
|
@ -110,6 +112,13 @@ export default function Facility3DViewerPage() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const targetedPlantTag = searchParams.get('plant');
|
const targetedPlantTag = searchParams.get('plant');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If coming from deep link, skip intro
|
||||||
|
if (targetedPlantTag) {
|
||||||
|
setShowIntro(false);
|
||||||
|
}
|
||||||
|
}, [targetedPlantTag]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -295,6 +304,12 @@ export default function Facility3DViewerPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full relative bg-slate-950 text-white overflow-hidden font-sans">
|
<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 */}
|
{/* 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="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">
|
<div className="pointer-events-auto flex items-center gap-3">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue