feat: add demo data for emails, files, filters, identities, mailboxes, vacation responses, and JMAP client interface
- 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.
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user