- Created demo emails with various states (inbox, sent, drafts, trash, etc.) in `emails.ts`. - Added demo file nodes representing directories and files in `files.ts`. - Implemented demo Sieve capabilities and scripts in `filters.ts`. - Defined demo identities for users in `identities.ts`. - Established demo mailboxes with permissions and counts in `mailboxes.ts`. - Created a demo vacation response in `vacation.ts`. - Introduced a comprehensive JMAP client interface in `client-interface.ts` to standardize interactions with the JMAP API.
149 lines
4.1 KiB
TypeScript
149 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from "react";
|
|
import { useRouter } from "@/i18n/navigation";
|
|
import { useAuthStore } from "@/stores/auth-store";
|
|
import { useCalendarStore } from "@/stores/calendar-store";
|
|
import { useWebDAVStore } from "@/stores/webdav-store";
|
|
import { getTourSteps, type TourStep } from "./tour-steps";
|
|
import { TourOverlay } from "./tour-overlay";
|
|
|
|
const TOUR_COMPLETED_KEY = "tour_completed";
|
|
const TOUR_CURRENT_STEP_KEY = "tour_current_step";
|
|
|
|
interface TourContextValue {
|
|
isActive: boolean;
|
|
currentStep: number;
|
|
totalSteps: number;
|
|
steps: TourStep[];
|
|
startTour: () => void;
|
|
stopTour: () => void;
|
|
nextStep: () => void;
|
|
prevStep: () => void;
|
|
hasCompletedTour: boolean;
|
|
resetTourCompletion: () => void;
|
|
}
|
|
|
|
const TourContext = createContext<TourContextValue | null>(null);
|
|
|
|
export function useTour() {
|
|
const ctx = useContext(TourContext);
|
|
if (!ctx) throw new Error("useTour must be used within TourProvider");
|
|
return ctx;
|
|
}
|
|
|
|
export function TourProvider({ children }: { children: ReactNode }) {
|
|
const router = useRouter();
|
|
const { isDemoMode } = useAuthStore();
|
|
const { supportsCalendar } = useCalendarStore();
|
|
const { supportsWebDAV } = useWebDAVStore();
|
|
|
|
const [isActive, setIsActive] = useState(false);
|
|
const [currentStep, setCurrentStep] = useState(0);
|
|
const [hasCompletedTour, setHasCompletedTour] = useState(false);
|
|
|
|
const steps = getTourSteps({ isDemoMode, supportsCalendar, supportsWebDAV: supportsWebDAV !== false });
|
|
|
|
useEffect(() => {
|
|
try {
|
|
setHasCompletedTour(localStorage.getItem(TOUR_COMPLETED_KEY) === "true");
|
|
} catch { /* */ }
|
|
}, []);
|
|
|
|
const startTour = useCallback(() => {
|
|
let resumeStep = 0;
|
|
try {
|
|
const stored = localStorage.getItem(TOUR_CURRENT_STEP_KEY);
|
|
if (stored) {
|
|
const parsed = parseInt(stored, 10);
|
|
if (!isNaN(parsed) && parsed >= 0) resumeStep = parsed;
|
|
}
|
|
} catch { /* */ }
|
|
|
|
// If the resume step is beyond the current steps, start from 0
|
|
if (resumeStep >= steps.length) resumeStep = 0;
|
|
|
|
setCurrentStep(resumeStep);
|
|
setIsActive(true);
|
|
}, [steps.length]);
|
|
|
|
const stopTour = useCallback(() => {
|
|
setIsActive(false);
|
|
try {
|
|
localStorage.removeItem(TOUR_CURRENT_STEP_KEY);
|
|
} catch { /* */ }
|
|
}, []);
|
|
|
|
const completeTour = useCallback(() => {
|
|
setIsActive(false);
|
|
setHasCompletedTour(true);
|
|
try {
|
|
localStorage.setItem(TOUR_COMPLETED_KEY, "true");
|
|
localStorage.removeItem(TOUR_CURRENT_STEP_KEY);
|
|
} catch { /* */ }
|
|
}, []);
|
|
|
|
const nextStep = useCallback(() => {
|
|
if (currentStep >= steps.length - 1) {
|
|
completeTour();
|
|
return;
|
|
}
|
|
const next = currentStep + 1;
|
|
const nextStepDef = steps[next];
|
|
setCurrentStep(next);
|
|
try {
|
|
localStorage.setItem(TOUR_CURRENT_STEP_KEY, String(next));
|
|
} catch { /* */ }
|
|
|
|
// Navigate if the next step requires a different page
|
|
if (nextStepDef?.page) {
|
|
router.push(nextStepDef.page);
|
|
}
|
|
}, [currentStep, steps, completeTour, router]);
|
|
|
|
const prevStep = useCallback(() => {
|
|
if (currentStep <= 0) return;
|
|
const prev = currentStep - 1;
|
|
const prevStepDef = steps[prev];
|
|
setCurrentStep(prev);
|
|
try {
|
|
localStorage.setItem(TOUR_CURRENT_STEP_KEY, String(prev));
|
|
} catch { /* */ }
|
|
|
|
if (prevStepDef?.page) {
|
|
router.push(prevStepDef.page);
|
|
} else if (steps[currentStep]?.page) {
|
|
// Going back from a page-specific step to a non-page step => go to mail
|
|
router.push("/");
|
|
}
|
|
}, [currentStep, steps, router]);
|
|
|
|
const resetTourCompletion = useCallback(() => {
|
|
setHasCompletedTour(false);
|
|
try {
|
|
localStorage.removeItem(TOUR_COMPLETED_KEY);
|
|
localStorage.removeItem(TOUR_CURRENT_STEP_KEY);
|
|
} catch { /* */ }
|
|
}, []);
|
|
|
|
const value: TourContextValue = {
|
|
isActive,
|
|
currentStep,
|
|
totalSteps: steps.length,
|
|
steps,
|
|
startTour,
|
|
stopTour,
|
|
nextStep,
|
|
prevStep,
|
|
hasCompletedTour,
|
|
resetTourCompletion,
|
|
};
|
|
|
|
return (
|
|
<TourContext.Provider value={value}>
|
|
{children}
|
|
{isActive && <TourOverlay />}
|
|
</TourContext.Provider>
|
|
);
|
|
}
|