Add src/components/dashboard/GuidedTour.jsx

This commit is contained in:
Eric Lay 2026-03-11 08:40:31 -05:00
parent f3bbca8773
commit 80f44eba59
1 changed files with 241 additions and 0 deletions

View File

@ -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>
);
}