Add src/components/dashboard/GuidedTour.jsx
This commit is contained in:
parent
f3bbca8773
commit
80f44eba59
|
|
@ -0,0 +1,241 @@
|
|||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { X, ChevronRight, ChevronLeft, Sparkles } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const tourSteps = [
|
||||
{
|
||||
targetId: "user-menu-btn",
|
||||
title: "User Actions & Account Controls",
|
||||
description: "This area contains your account actions and user controls. Access your profile, contact support, manage store information, and sign out from here.",
|
||||
position: "bottom-right",
|
||||
},
|
||||
{
|
||||
targetId: "notifications-btn",
|
||||
title: "Notifications & Alerts",
|
||||
description: "This bell icon is your notification center. You'll receive important system alerts, bulletins, program updates, and action items here. Unread notifications are shown with a red badge count.",
|
||||
position: "bottom-right",
|
||||
},
|
||||
{
|
||||
targetId: "dashboard-main",
|
||||
title: "Your Performance Dashboard",
|
||||
description: "This is your main dashboard — track parcels received, consolidations completed, financial earnings, and compliance events all in one place.",
|
||||
position: "center",
|
||||
},
|
||||
{
|
||||
targetId: "sidebar",
|
||||
title: "Navigation & Settings",
|
||||
description: "Use the sidebar to navigate between Program Management, Communication & Support, and Member Management. Access your profile and Stripe Connect settings here.",
|
||||
position: "right",
|
||||
},
|
||||
{
|
||||
targetId: null,
|
||||
title: "Complete Your Stripe Onboarding",
|
||||
description: "You're almost done! The final step is to set up your Stripe Express account so you can receive electronic program payouts from PackageHub.",
|
||||
position: "center",
|
||||
isFinal: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function GuidedTour({ onComplete }) {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [spotlightRect, setSpotlightRect] = useState(null);
|
||||
const [tooltipStyle, setTooltipStyle] = useState({});
|
||||
|
||||
const calculatePosition = useCallback(() => {
|
||||
const step = tourSteps[currentStep];
|
||||
if (!step.targetId) {
|
||||
setSpotlightRect(null);
|
||||
setTooltipStyle({
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const el = document.getElementById(step.targetId);
|
||||
if (!el) {
|
||||
setSpotlightRect(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
const padding = 8;
|
||||
setSpotlightRect({
|
||||
top: rect.top - padding,
|
||||
left: rect.left - padding,
|
||||
width: rect.width + padding * 2,
|
||||
height: rect.height + padding * 2,
|
||||
});
|
||||
|
||||
// Calculate tooltip position
|
||||
let style = {};
|
||||
if (step.position === "bottom-right") {
|
||||
style = {
|
||||
top: rect.bottom + 16,
|
||||
right: window.innerWidth - rect.right,
|
||||
};
|
||||
} else if (step.position === "right") {
|
||||
style = {
|
||||
top: rect.top + 60,
|
||||
left: rect.right + 16,
|
||||
};
|
||||
} else {
|
||||
style = {
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
};
|
||||
}
|
||||
setTooltipStyle(style);
|
||||
}, [currentStep]);
|
||||
|
||||
useEffect(() => {
|
||||
calculatePosition();
|
||||
window.addEventListener("resize", calculatePosition);
|
||||
return () => window.removeEventListener("resize", calculatePosition);
|
||||
}, [calculatePosition]);
|
||||
|
||||
const step = tourSteps[currentStep];
|
||||
const isLast = currentStep === tourSteps.length - 1;
|
||||
const isFirst = currentStep === 0;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
{/* Dark overlay with spotlight cutout */}
|
||||
<svg className="absolute inset-0 w-full h-full" style={{ pointerEvents: "none" }}>
|
||||
<defs>
|
||||
<mask id="spotlight-mask">
|
||||
<rect width="100%" height="100%" fill="white" />
|
||||
{spotlightRect && (
|
||||
<rect
|
||||
x={spotlightRect.left}
|
||||
y={spotlightRect.top}
|
||||
width={spotlightRect.width}
|
||||
height={spotlightRect.height}
|
||||
rx="12"
|
||||
fill="black"
|
||||
/>
|
||||
)}
|
||||
</mask>
|
||||
</defs>
|
||||
<rect
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="rgba(0,0,0,0.6)"
|
||||
mask="url(#spotlight-mask)"
|
||||
style={{ pointerEvents: "auto" }}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* Spotlight glow ring */}
|
||||
{spotlightRect && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="absolute rounded-xl border-2 border-[#e67e22] shadow-[0_0_0_4px_rgba(230,126,34,0.2)]"
|
||||
style={{
|
||||
top: spotlightRect.top,
|
||||
left: spotlightRect.left,
|
||||
width: spotlightRect.width,
|
||||
height: spotlightRect.height,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tooltip */}
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentStep}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="absolute z-10"
|
||||
style={{ ...tooltipStyle, pointerEvents: "auto" }}
|
||||
>
|
||||
<div className="bg-white rounded-2xl shadow-2xl border border-gray-200 w-[380px] overflow-hidden">
|
||||
{/* Step indicator row */}
|
||||
<div className="px-6 pt-5 pb-0 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-amber-400" />
|
||||
<span className="text-sm font-medium text-[#1a5276]">
|
||||
Step {currentStep + 1} of {tourSteps.length}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onComplete}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Logo */}
|
||||
<div className="flex justify-center pt-3 pb-1">
|
||||
<img
|
||||
src="https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/69ac537c5138f01ec706f4bc/9c287bb3b_Logo_PBC_horz_color.png"
|
||||
alt="PackageHub Business Centers"
|
||||
className="h-8 w-auto object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-6 pt-3 pb-5">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-2">{step.title}</h3>
|
||||
<p className="text-sm text-gray-600 leading-relaxed">{step.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Progress dots */}
|
||||
<div className="flex justify-center gap-1.5 pb-4">
|
||||
{tourSteps.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-2 rounded-full transition-all ${
|
||||
i === currentStep ? "bg-[#1a5276] w-6" : i < currentStep ? "bg-[#3498db] w-2" : "bg-gray-200 w-2"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="px-6 py-4 border-t border-gray-100 flex items-center justify-end">
|
||||
<div className="flex gap-2">
|
||||
{!isFirst && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentStep((s) => s - 1)}
|
||||
className="rounded-lg"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4 mr-1" />
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (isLast) onComplete();
|
||||
else setCurrentStep((s) => s + 1);
|
||||
}}
|
||||
className="bg-[#1a5276] hover:bg-[#154360] rounded-lg"
|
||||
>
|
||||
{isLast ? (
|
||||
"Go to Stripe Setup"
|
||||
) : (
|
||||
<>
|
||||
Next
|
||||
<ChevronRight className="w-4 h-4 ml-1" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue