feat: add TourProgressController and related entities for user tour progress tracking
- 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.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user