- Implemented TourProgressController to handle API endpoints for tracking guided tours seen by users. - Created UserTourProgress entity to store the highest version of tours seen by each user. - Developed UserTourProgressRepository for database interactions related to user tour progress. - Introduced TourProgressService to manage business logic for marking tours as seen and retrieving seen maps. - Added comprehensive tests for API endpoints and entity behavior to ensure functionality and data integrity.
77 lines
3.1 KiB
TypeScript
77 lines
3.1 KiB
TypeScript
import { useCallback, useEffect, useRef } from 'react';
|
|
import { driver } from 'driver.js';
|
|
import 'driver.js/dist/driver.css';
|
|
import { getTour } from '../lib/tour/registry';
|
|
import { anchorSelector, resolveSteps } from '../lib/tour/resolveSteps';
|
|
import { formatNumber } from '../lib/utils';
|
|
import { useTourProgress } from './useTourProgress';
|
|
|
|
interface Options {
|
|
/** وقتی true شد یعنی دادهٔ صفحه آمده و المانهای هدف رندر شدهاند */
|
|
ready?: boolean;
|
|
}
|
|
|
|
/**
|
|
* راهنمای قدمبهقدم یک صفحه. بار اول خودکار اجرا میشود و بعد از آن فقط با
|
|
* صدا زدن start — یعنی دکمهٔ «؟» صفحه.
|
|
*/
|
|
export function useTour(tourId?: string, { ready = false }: Options = {}) {
|
|
const tour = getTour(tourId);
|
|
const { isReady, isSeen, markSeen } = useTourProgress();
|
|
const autoStarted = useRef(false);
|
|
|
|
const start = useCallback(() => {
|
|
if (!tour) return;
|
|
|
|
const steps = resolveSteps(tour.steps);
|
|
if (steps.length === 0) return;
|
|
|
|
driver({
|
|
showProgress: true,
|
|
allowClose: true,
|
|
overlayOpacity: 0.55,
|
|
popoverClass: 'cp-tour',
|
|
nextBtnText: 'بعدی',
|
|
prevBtnText: 'قبلی',
|
|
doneBtnText: 'باشه، فهمیدم',
|
|
steps: steps.map((s, i) => ({
|
|
element: anchorSelector(s.anchor),
|
|
popover: {
|
|
title: s.title,
|
|
description: s.body,
|
|
side: s.side ?? 'bottom',
|
|
align: 'start',
|
|
// قالبِ سراسری driver فقط {{current}} میدهد و آن ارقام لاتین است؛
|
|
// شمارنده باید مثل بقیهٔ پنل فارسی باشد.
|
|
progressText: `${formatNumber(i + 1)} از ${formatNumber(steps.length)}`,
|
|
},
|
|
})),
|
|
// بستن وسط تور هم «دیده شده» است؛ تکرارش برای کسی که ردش کرده آزار است.
|
|
onDestroyed: () => markSeen(tour.id, tour.version),
|
|
}).drive();
|
|
}, [tour, markSeen]);
|
|
|
|
// `start` با هر رندر بازساخته میشود؛ اگر وابستگیِ effect باشد، cleanup تایمرِ
|
|
// سیصد میلیثانیهای را قبل از شلیک پاک میکند و تور هرگز اجرا نمیشود.
|
|
const startRef = useRef(start);
|
|
startRef.current = start;
|
|
|
|
// مقدار boolean وابستگیِ پایداری است، برخلاف خودِ تابع isSeen.
|
|
const alreadySeen = tour ? isSeen(tour.id, tour.version) : true;
|
|
|
|
useEffect(() => {
|
|
if (!tour || !ready || autoStarted.current) return;
|
|
// تا پاسخ سرور نیامده هیچ چیز اجرا نمیشود؛ وگرنه در خطای شبکه کاربر قدیمی
|
|
// هر بار رفرش یک تور میبیند.
|
|
if (!isReady || alreadySeen) return;
|
|
|
|
autoStarted.current = true;
|
|
// یک لحظه صبر تا چیدمان نهایی بنشیند و highlight سرِ جای درست بیفتد.
|
|
const timer = window.setTimeout(() => startRef.current(), 300);
|
|
|
|
return () => window.clearTimeout(timer);
|
|
}, [tour, ready, isReady, alreadySeen]);
|
|
|
|
return { available: tour !== null, start };
|
|
}
|