update merchantos svg
This commit is contained in:
467
src/components/platform/AnimatedDashboard.tsx
Normal file
467
src/components/platform/AnimatedDashboard.tsx
Normal file
@@ -0,0 +1,467 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion, AnimatePresence, useReducedMotion } from "framer-motion";
|
||||
|
||||
type ProductAsset = {
|
||||
emoji: string;
|
||||
name: string;
|
||||
xStart: string; // Tailwind coordinate
|
||||
yStart: string; // Tailwind coordinate
|
||||
floatDelay: number;
|
||||
floatDuration: number;
|
||||
};
|
||||
|
||||
// Define products that float and participate in AI automation
|
||||
const PRODUCT_ASSETS: ProductAsset[] = [
|
||||
{ emoji: "🍎", name: "Apple", xStart: "left-[5%]", yStart: "top-[10%]", floatDelay: 0, floatDuration: 4 },
|
||||
{ emoji: "🥛", name: "Milk", xStart: "right-[5%]", yStart: "top-[15%]", floatDelay: 0.5, floatDuration: 4.5 },
|
||||
{ emoji: "🍞", name: "Bread", xStart: "left-[0%]", yStart: "top-[45%]", floatDelay: 0.8, floatDuration: 4.8 },
|
||||
{ emoji: "🥬", name: "Spinach", xStart: "right-[2%]", yStart: "top-[50%]", floatDelay: 1.2, floatDuration: 5.2 },
|
||||
{ emoji: "🏷️", name: "Price Tag", xStart: "left-[8%]", yStart: "bottom-[12%]", floatDelay: 1.5, floatDuration: 4.2 },
|
||||
{ emoji: "📲", name: "QR Code", xStart: "right-[8%]", yStart: "bottom-[15%]", floatDelay: 2, floatDuration: 4.6 },
|
||||
];
|
||||
|
||||
export function AnimatedDashboard() {
|
||||
const reduce = useReducedMotion();
|
||||
|
||||
// Animation states
|
||||
const [animatingIndex, setAnimatingIndex] = useState(0);
|
||||
const [stage, setStage] = useState<"idle" | "flying" | "scanning" | "updating" | "notifying">("idle");
|
||||
const [itemsCount, setItemsCount] = useState(142);
|
||||
const [ordersCount, setOrdersCount] = useState(8);
|
||||
|
||||
useEffect(() => {
|
||||
if (reduce) return;
|
||||
|
||||
const cycle = setInterval(() => {
|
||||
// 1. Start Fly-in phase
|
||||
setStage("flying");
|
||||
|
||||
// 2. Scan phase: Product reaches center, trigger laser scan
|
||||
const scanTimer = setTimeout(() => {
|
||||
setStage("scanning");
|
||||
}, 900);
|
||||
|
||||
// 3. Update dashboard values (counter, green success pulse)
|
||||
const updateTimer = setTimeout(() => {
|
||||
setStage("updating");
|
||||
|
||||
// Update specific counters based on what product is processed
|
||||
if (animatingIndex === 0) {
|
||||
setItemsCount((c) => c + 1);
|
||||
} else if (animatingIndex === 1) {
|
||||
setOrdersCount((c) => c + 1);
|
||||
}
|
||||
}, 1400);
|
||||
|
||||
// 4. Micro-notification appears
|
||||
const notifyTimer = setTimeout(() => {
|
||||
setStage("notifying");
|
||||
}, 1800);
|
||||
|
||||
// 5. Clear notification, return to idle
|
||||
const idleTimer = setTimeout(() => {
|
||||
setStage("idle");
|
||||
setAnimatingIndex((prev) => (prev + 1) % 4); // Cycle through the first 4 products
|
||||
}, 5200);
|
||||
|
||||
return () => {
|
||||
clearTimeout(scanTimer);
|
||||
clearTimeout(updateTimer);
|
||||
clearTimeout(notifyTimer);
|
||||
clearTimeout(idleTimer);
|
||||
};
|
||||
}, 8000); // Complete animation cycle every 8 seconds
|
||||
|
||||
return () => clearInterval(cycle);
|
||||
}, [animatingIndex, reduce]);
|
||||
|
||||
// Floating variants for the idle products surrounding the dashboard
|
||||
const floatVariant = (delay: number, duration: number) => ({
|
||||
animate: {
|
||||
y: [0, -10, 0],
|
||||
rotate: [-3, 3, -3],
|
||||
scale: [1, 1.02, 1],
|
||||
transition: {
|
||||
duration,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Curved flight coordinates relative to the absolute dashboard layout
|
||||
// Product index maps to original coordinates:
|
||||
// index 0 (Apple): left-5%, top-10% (x: 5%, y: 10%) -> center (x: 50%, y: 45%)
|
||||
const flightPaths = [
|
||||
{ x: ["5%", "25%", "50%"], y: ["10%", "20%", "45%"] }, // Apple
|
||||
{ x: ["95%", "75%", "50%"], y: ["15%", "25%", "45%"] }, // Milk
|
||||
{ x: ["0%", "20%", "50%"], y: ["45%", "40%", "45%"] }, // Bread
|
||||
{ x: ["98%", "78%", "50%"], y: ["50%", "45%", "45%"] }, // Spinach
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative w-full aspect-[440/336] flex items-center justify-center p-8 sm:p-12 overflow-visible select-none">
|
||||
{/* ── Background Glow ── */}
|
||||
<div className="absolute inset-0 -z-20 bg-gradient-to-tr from-purple-500/10 to-[#6330D6]/10 blur-3xl rounded-full pointer-events-none scale-90" />
|
||||
|
||||
{/* ── 1. Floating Product Assets (Around Dashboard) ── */}
|
||||
{PRODUCT_ASSETS.map((product, idx) => {
|
||||
// Hide specific products on tablet/mobile screens
|
||||
let responsiveClass = "absolute z-20 ";
|
||||
if (idx === 2 || idx === 3) {
|
||||
responsiveClass += "hidden lg:block"; // Only desktop
|
||||
} else if (idx === 0 || idx === 1 || idx === 4 || idx === 5) {
|
||||
responsiveClass += "hidden md:block"; // Desktop & Tablet
|
||||
}
|
||||
|
||||
const isCurrentlyAnimating = stage !== "idle" && animatingIndex === idx;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={product.name}
|
||||
className={`${responsiveClass} ${product.xStart} ${product.yStart}`}
|
||||
variants={floatVariant(product.floatDelay, product.floatDuration)}
|
||||
animate={isCurrentlyAnimating ? "exit" : "animate"}
|
||||
style={{ opacity: isCurrentlyAnimating ? 0.15 : 1 }}
|
||||
>
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl border border-purple-500/10 bg-white/75 backdrop-blur-md shadow-[0_4px_16px_rgba(99,48,214,0.04)] text-lg">
|
||||
{product.emoji}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* ── 2. Flying Product Scan Asset ── */}
|
||||
<AnimatePresence>
|
||||
{stage === "flying" && animatingIndex < 4 && (
|
||||
<motion.div
|
||||
initial={{
|
||||
x: flightPaths[animatingIndex].x[0],
|
||||
y: flightPaths[animatingIndex].y[0],
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
}}
|
||||
animate={{
|
||||
x: flightPaths[animatingIndex].x,
|
||||
y: flightPaths[animatingIndex].y,
|
||||
opacity: [1, 1, 0.2],
|
||||
scale: [1, 0.7, 0.4],
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.9,
|
||||
ease: [0.25, 0.1, 0.25, 1],
|
||||
}}
|
||||
className="absolute z-30 pointer-events-none flex h-11 w-11 items-center justify-center rounded-2xl border-2 border-[#7C3AED] bg-white shadow-[0_0_15px_#7C3AED] text-lg"
|
||||
>
|
||||
{PRODUCT_ASSETS[animatingIndex].emoji}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* ── 3. AI Processing Particle Trails ── */}
|
||||
{stage === "flying" && animatingIndex < 4 && (
|
||||
<>
|
||||
{[0, 1, 2].map((particleIdx) => (
|
||||
<motion.div
|
||||
key={particleIdx}
|
||||
initial={{
|
||||
x: flightPaths[animatingIndex].x[0],
|
||||
y: flightPaths[animatingIndex].y[0],
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
}}
|
||||
animate={{
|
||||
x: flightPaths[animatingIndex].x,
|
||||
y: flightPaths[animatingIndex].y,
|
||||
opacity: [0, 0.7, 0],
|
||||
scale: [0.6, 0.3, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.9,
|
||||
delay: 0.08 * (particleIdx + 1),
|
||||
ease: "easeOut",
|
||||
}}
|
||||
className="absolute z-28 w-1.5 h-1.5 rounded-full bg-[#7C3AED] shadow-[0_0_8px_#7C3AED]"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── 4. Slide-in Micro-Notifications ── */}
|
||||
<AnimatePresence>
|
||||
{stage === "notifying" && (
|
||||
<motion.div
|
||||
initial={{ y: -25, opacity: 0, x: "-50%" }}
|
||||
animate={{ y: 0, opacity: 1, x: "-50%" }}
|
||||
exit={{ y: -20, opacity: 0, x: "-50%" }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 24 }}
|
||||
className="absolute top-[22%] left-1/2 z-40 bg-neutral-950 text-white rounded-full px-4 py-1.5 shadow-lg border border-purple-500/20 flex items-center gap-2 whitespace-nowrap"
|
||||
>
|
||||
<span className="text-emerald-400 text-xs font-bold">✓</span>
|
||||
<span className="text-[10px] sm:text-[11px] font-bold tracking-tight">
|
||||
{animatingIndex === 0 && "Apple Catalog Listing Synced"}
|
||||
{animatingIndex === 1 && "Milk Delivery Stock Updated"}
|
||||
{animatingIndex === 2 && "Bread Pricing Configuration Live"}
|
||||
{animatingIndex === 3 && "Spinach Category Catalog Indexed"}
|
||||
</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* ── 5. Main Dashboard Canvas ── */}
|
||||
<div className="relative w-full h-full flex items-center justify-center">
|
||||
{/* Ambient Success Pulse Border */}
|
||||
<motion.div
|
||||
animate={
|
||||
stage === "updating"
|
||||
? {
|
||||
borderColor: ["rgba(99,48,214,0.1)", "rgba(34,197,94,0.6)", "rgba(99,48,214,0.1)"],
|
||||
boxShadow: [
|
||||
"0 0 0 rgba(34,197,94,0)",
|
||||
"0 0 20px rgba(34,197,94,0.2)",
|
||||
"0 0 0 rgba(34,197,94,0)",
|
||||
],
|
||||
}
|
||||
: {}
|
||||
}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="absolute inset-[30px] rounded-[22px] border-[1.5px] border-[#EDE9FE]/0 pointer-events-none z-10"
|
||||
/>
|
||||
|
||||
<motion.svg
|
||||
viewBox="0 0 440 336"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-full h-auto relative z-0"
|
||||
>
|
||||
{/* Ground shadow */}
|
||||
<ellipse cx="220" cy="312" rx="150" ry="14" fill="#6330D6" opacity="0.08" />
|
||||
|
||||
{/* Main dashboard window */}
|
||||
<g>
|
||||
<rect x="40" y="30" width="360" height="256" rx="22" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.5" />
|
||||
{/* Top window bar chrome */}
|
||||
<circle cx="66" cy="54" r="4" fill="#E6D9F7" />
|
||||
<circle cx="80" cy="54" r="4" fill="#E6D9F7" />
|
||||
<circle cx="94" cy="54" r="4" fill="#E6D9F7" />
|
||||
<rect x="150" y="48" width="140" height="13" rx="6.5" fill="#F3E8F6" />
|
||||
<line x1="40" y1="72" x2="400" y2="72" stroke="#F0E9F8" strokeWidth="1.5" />
|
||||
</g>
|
||||
|
||||
{/* Sidebar navigation */}
|
||||
<g>
|
||||
<rect x="52" y="82" width="58" height="192" rx="14" fill="#FAF7FD" />
|
||||
<rect x="64" y="98" width="34" height="8" rx="4" fill="#6330D6" />
|
||||
<rect x="64" y="120" width="30" height="7" rx="3.5" fill="#D9CBEF" />
|
||||
<rect x="64" y="138" width="34" height="7" rx="3.5" fill="#D9CBEF" />
|
||||
<rect x="64" y="156" width="26" height="7" rx="3.5" fill="#D9CBEF" />
|
||||
<rect x="64" y="174" width="32" height="7" rx="3.5" fill="#D9CBEF" />
|
||||
|
||||
{/* QR Ordering live badge */}
|
||||
<g>
|
||||
<motion.rect
|
||||
x="64"
|
||||
y="210"
|
||||
width="34"
|
||||
height="12"
|
||||
rx="4"
|
||||
fill="#6330D6"
|
||||
animate={{
|
||||
fill: stage === "updating" && animatingIndex === 3 ? "#22C55E" : "#6330D6",
|
||||
opacity: stage === "updating" && animatingIndex === 3 ? 0.25 : 0.15,
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
/>
|
||||
<motion.text
|
||||
x="68"
|
||||
y="218"
|
||||
fontSize="5"
|
||||
fontWeight="bold"
|
||||
animate={{
|
||||
fill: stage === "updating" && animatingIndex === 3 ? "#16A34A" : "#6330D6",
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
QR ACTIVE
|
||||
</motion.text>
|
||||
</g>
|
||||
|
||||
{/* Loyalty active badge */}
|
||||
<g>
|
||||
<rect x="64" y="226" width="34" height="12" rx="4" fill="#6330D6" opacity="0.1" />
|
||||
<text x="69" y="234" fill="#6330D6" fontSize="5" fontWeight="bold">LOYALTY</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
{/* ── KPI Chips ── */}
|
||||
{/* KPI 1: Inventory Items */}
|
||||
<g>
|
||||
<rect x="124" y="84" width="80" height="46" rx="12" fill="#F5F1FB" />
|
||||
<text x="136" y="97" fill="#8B7FB5" fontSize="7" fontWeight="bold">ITEMS</text>
|
||||
|
||||
{/* Animate number change */}
|
||||
<text x="136" y="118" fill="#16001D" fontSize="13" fontWeight="900" className="font-display">
|
||||
{itemsCount}
|
||||
</text>
|
||||
|
||||
{/* Micro pulse indicator */}
|
||||
<motion.circle
|
||||
cx="188"
|
||||
cy="95"
|
||||
r="3.5"
|
||||
fill="#6330D6"
|
||||
initial={{ scale: 0.8 }}
|
||||
animate={stage === "updating" && animatingIndex === 0 ? { scale: [1, 1.8, 1], opacity: [0.8, 0, 0.8] } : {}}
|
||||
transition={{ duration: 0.6 }}
|
||||
/>
|
||||
</g>
|
||||
|
||||
{/* KPI 2: Sales Counter */}
|
||||
<g>
|
||||
<rect x="212" y="84" width="80" height="46" rx="12" fill="#ECFBF1" />
|
||||
<text x="224" y="97" fill="#008037" fontSize="7" fontWeight="bold">SALES</text>
|
||||
<text x="224" y="118" fill="#007239" fontSize="13" fontWeight="900" className="font-display">
|
||||
₹18,240
|
||||
</text>
|
||||
</g>
|
||||
|
||||
{/* KPI 3: Orders Counter */}
|
||||
<g>
|
||||
<rect x="300" y="84" width="80" height="46" rx="12" fill="#FFF7E8" />
|
||||
<text x="312" y="97" fill="#C28C00" fontSize="7" fontWeight="bold">ORDERS</text>
|
||||
|
||||
{/* Animate orders increase */}
|
||||
<text x="312" y="118" fill="#D97706" fontSize="13" fontWeight="900" className="font-display">
|
||||
{ordersCount}
|
||||
</text>
|
||||
|
||||
<motion.circle
|
||||
cx="364"
|
||||
cy="95"
|
||||
r="3"
|
||||
fill="#D97706"
|
||||
initial={{ scale: 0.8 }}
|
||||
animate={stage === "updating" && animatingIndex === 1 ? { scale: [1, 1.8, 1], opacity: [0.8, 0, 0.8] } : {}}
|
||||
transition={{ duration: 0.6 }}
|
||||
/>
|
||||
</g>
|
||||
|
||||
{/* ── Chart Panel (Middle Area) ── */}
|
||||
<g>
|
||||
<rect x="124" y="142" width="168" height="120" rx="14" fill="#FCFAFF" stroke="#F0E9F8" strokeWidth="1.2" />
|
||||
<rect x="138" y="156" width="44" height="8" rx="4" fill="#C9B8EC" />
|
||||
|
||||
{/* Gridlines */}
|
||||
<line x1="138" y1="196" x2="278" y2="196" stroke="#EFE7FB" strokeWidth="1" />
|
||||
<line x1="138" y1="222" x2="278" y2="222" stroke="#EFE7FB" strokeWidth="1" />
|
||||
|
||||
{/* Dynamic line graph */}
|
||||
<motion.path
|
||||
d="M140 236 L168 218 L196 224 L224 196 L252 204 L278 176"
|
||||
fill="none"
|
||||
stroke="#6330D6"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
animate={
|
||||
stage === "updating" && animatingIndex === 2
|
||||
? {
|
||||
stroke: ["#6330D6", "#10B981", "#6330D6"],
|
||||
strokeWidth: [2.5, 3.5, 2.5],
|
||||
}
|
||||
: {}
|
||||
}
|
||||
transition={{ duration: 0.8 }}
|
||||
/>
|
||||
|
||||
{/* Pulse node */}
|
||||
<motion.circle
|
||||
cx="278"
|
||||
cy="176"
|
||||
r="4"
|
||||
fill="#6330D6"
|
||||
stroke="#fff"
|
||||
strokeWidth="1.5"
|
||||
animate={stage === "updating" && animatingIndex === 2 ? { scale: [1, 1.6, 1] } : {}}
|
||||
/>
|
||||
</g>
|
||||
|
||||
{/* ── Mini Bar Chart ── */}
|
||||
<g>
|
||||
<rect x="300" y="142" width="80" height="120" rx="14" fill="#FCFAFF" stroke="#F0E9F8" strokeWidth="1.2" />
|
||||
|
||||
{[
|
||||
{ x: 314, h: 34, c: "#DDD0F4" },
|
||||
{ x: 332, h: 52, c: "#B99BEC" },
|
||||
{ x: 350, h: 44, c: "#9B78E4" },
|
||||
{ x: 368, h: 66, c: "#6330D6" },
|
||||
].map((b, bIdx) => {
|
||||
// If index 2 product (price/pricing) updates, scale the heights briefly
|
||||
const isTargetBar = bIdx === 2 || bIdx === 3;
|
||||
const multiplier = stage === "updating" && animatingIndex === 2 && isTargetBar ? 1.25 : 1;
|
||||
const barHeight = b.h * multiplier;
|
||||
|
||||
return (
|
||||
<motion.rect
|
||||
key={b.x}
|
||||
x={b.x}
|
||||
y={250 - barHeight}
|
||||
width="11"
|
||||
height={barHeight}
|
||||
rx="4"
|
||||
fill={b.c}
|
||||
transition={{ type: "spring", stiffness: 100, damping: 12 }}
|
||||
style={{ transformBox: "fill-box", transformOrigin: "bottom" }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
|
||||
{/* ── Floating order card top right (floating) ── */}
|
||||
<motion.g
|
||||
animate={{ y: [0, -6, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 3.6, ease: "easeInOut" }}
|
||||
>
|
||||
<rect x="316" y="24" width="104" height="42" rx="13" fill="#16001D" />
|
||||
<circle cx="336" cy="45" r="10" fill="#7C3AED" />
|
||||
<path d="M332 45 l3 3 l6 -6.5" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
<rect x="352" y="38" width="52" height="6" rx="3" fill="#ffffff" opacity="0.9" />
|
||||
<rect x="352" y="49" width="34" height="5" rx="2.5" fill="#C9A2FF" />
|
||||
</motion.g>
|
||||
|
||||
{/* ── Floating live stock status bottom left (floating) ── */}
|
||||
<motion.g
|
||||
animate={{ y: [0, 6, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 4.2, ease: "easeInOut" }}
|
||||
>
|
||||
<rect x="22" y="196" width="96" height="38" rx="12" fill="#FFFFFF" stroke="#EDE9FE" strokeWidth="1.5" />
|
||||
<circle cx="42" cy="215" r="6" fill="#22C55E" />
|
||||
<rect x="56" y="208" width="48" height="6" rx="3" fill="#16001D" opacity="0.75" />
|
||||
<rect x="56" y="219" width="30" height="5" rx="2.5" fill="#B9A6E0" />
|
||||
</motion.g>
|
||||
|
||||
{/* ── Purple AI Laser Scanning Beam ── */}
|
||||
{stage === "scanning" && (
|
||||
<motion.line
|
||||
x1="45"
|
||||
y1="34"
|
||||
x2="395"
|
||||
y2="34"
|
||||
stroke="#7C3AED"
|
||||
strokeWidth="2.5"
|
||||
initial={{ y: 0, opacity: 0 }}
|
||||
animate={{
|
||||
y: [0, 240, 0],
|
||||
opacity: [0, 0.85, 0.85, 0],
|
||||
}}
|
||||
transition={{ duration: 0.95, ease: "easeInOut" }}
|
||||
/>
|
||||
)}
|
||||
</motion.svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,7 @@
|
||||
import { useRef } from "react";
|
||||
import { motion, useReducedMotion, useScroll, useTransform } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { DashboardMockup } from "@/components/platform/DeviceShowcase";
|
||||
import { ConsumerShowcase } from "@/components/platform/ConsumerShowcase";
|
||||
import { AnimatedDashboard } from "@/components/platform/AnimatedDashboard";
|
||||
import { fadeUp, staggerContainer, staggerCards, viewport, viewport20 } from "@/lib/animations";
|
||||
|
||||
export type ProductFeature = {
|
||||
@@ -137,7 +136,7 @@ export function ProductFeatureGrid({
|
||||
"radial-gradient(circle at 50% 40%, rgba(99,48,214,0.16), transparent 62%)",
|
||||
}}
|
||||
/>
|
||||
{isPhone ? <ConsumerShowcase /> : <DashboardMockup />}
|
||||
<AnimatedDashboard />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user