import { Head, router } from '@inertiajs/react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useEffect, useState } from 'react' import AppLayout from '~/layouts/AppLayout' import StyledButton from '~/components/StyledButton' import api from '~/lib/api' import { ServiceSlim } from '../../../types/services' import CuratedCollectionCard from '~/components/CuratedCollectionCard' import LoadingSpinner from '~/components/LoadingSpinner' import Alert from '~/components/Alert' import { IconCheck } from '@tabler/icons-react' import { useNotifications } from '~/context/NotificationContext' import useInternetStatus from '~/hooks/useInternetStatus' import classNames from 'classnames' type WizardStep = 1 | 2 | 3 | 4 const CURATED_MAP_COLLECTIONS_KEY = 'curated-map-collections' const CURATED_ZIM_COLLECTIONS_KEY = 'curated-zim-collections' export default function EasySetupWizard(props: { system: { services: ServiceSlim[] } }) { const [currentStep, setCurrentStep] = useState(1) const [selectedServices, setSelectedServices] = useState([]) const [selectedMapCollections, setSelectedMapCollections] = useState([]) const [selectedZimCollections, setSelectedZimCollections] = useState([]) const [isProcessing, setIsProcessing] = useState(false) const { addNotification } = useNotifications() const { isOnline } = useInternetStatus() const queryClient = useQueryClient() const anySelectionMade = selectedServices.length > 0 || selectedMapCollections.length > 0 || selectedZimCollections.length > 0 const { data: mapCollections, isLoading: isLoadingMaps } = useQuery({ queryKey: [CURATED_MAP_COLLECTIONS_KEY], queryFn: () => api.listCuratedMapCollections(), refetchOnWindowFocus: false, }) const { data: zimCollections, isLoading: isLoadingZims } = useQuery({ queryKey: [CURATED_ZIM_COLLECTIONS_KEY], queryFn: () => api.listCuratedZimCollections(), refetchOnWindowFocus: false, }) const availableServices = props.system.services.filter( (service) => !service.installed && service.installation_status !== 'installing' ) const toggleServiceSelection = (serviceName: string) => { setSelectedServices((prev) => prev.includes(serviceName) ? prev.filter((s) => s !== serviceName) : [...prev, serviceName] ) } const toggleMapCollection = (slug: string) => { setSelectedMapCollections((prev) => prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug] ) } const toggleZimCollection = (slug: string) => { setSelectedZimCollections((prev) => prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug] ) } const canProceedToNextStep = () => { if (!isOnline) return false // Must be online to proceed if (currentStep === 1) return true // Can skip app installation if (currentStep === 2) return true // Can skip map downloads if (currentStep === 3) return true // Can skip ZIM downloads return false } const handleNext = () => { if (currentStep < 4) { setCurrentStep((prev) => (prev + 1) as WizardStep) } } const handleBack = () => { if (currentStep > 1) { setCurrentStep((prev) => (prev - 1) as WizardStep) } } const handleFinish = async () => { if (!isOnline) { addNotification({ type: 'error', message: 'You must have an internet connection to complete the setup.', }) return } setIsProcessing(true) try { // All of these ops don't actually wait for completion, they just kick off the process, so we can run them in parallel without awaiting each one sequentially const installPromises = selectedServices.map((serviceName) => api.installService(serviceName)) await Promise.all(installPromises) const downloadPromises = [ ...selectedMapCollections.map((slug) => api.downloadMapCollection(slug)), ...selectedZimCollections.map((slug) => api.downloadZimCollection(slug)), ] await Promise.all(downloadPromises) addNotification({ type: 'success', message: 'Setup wizard completed! Your selections are being processed.', }) router.visit('/easy-setup/complete') } catch (error) { console.error('Error during setup:', error) addNotification({ type: 'error', message: 'An error occurred during setup. Some items may not have been processed.', }) } finally { setIsProcessing(false) } } const fetchLatestMapCollections = useMutation({ mutationFn: () => api.fetchLatestMapCollections(), onSuccess: () => { addNotification({ message: 'Successfully fetched the latest map collections.', type: 'success', }) queryClient.invalidateQueries({ queryKey: [CURATED_MAP_COLLECTIONS_KEY] }) }, }) const fetchLatestZIMCollections = useMutation({ mutationFn: () => api.fetchLatestZimCollections(), onSuccess: () => { addNotification({ message: 'Successfully fetched the latest ZIM collections.', type: 'success', }) queryClient.invalidateQueries({ queryKey: [CURATED_ZIM_COLLECTIONS_KEY] }) }, }) // Auto-fetch latest collections if the list is empty useEffect(() => { if (mapCollections && mapCollections.length === 0 && !fetchLatestMapCollections.isPending) { fetchLatestMapCollections.mutate() } }, [mapCollections, fetchLatestMapCollections]) useEffect(() => { if (zimCollections && zimCollections.length === 0 && !fetchLatestZIMCollections.isPending) { fetchLatestZIMCollections.mutate() } }, [zimCollections, fetchLatestZIMCollections]) const renderStepIndicator = () => { const steps = [ { number: 1, label: 'Apps' }, { number: 2, label: 'Maps' }, { number: 3, label: 'ZIM Files' }, { number: 4, label: 'Review' }, ] return ( ) } const renderStep1 = () => (

Choose Apps to Install

Select the applications you'd like to install. You can always add more later.

{availableServices.length === 0 ? (

All available apps are already installed!

router.visit('/settings/apps')} > Manage Apps
) : (
{availableServices.map((service) => { const selected = selectedServices.includes(service.service_name) return (
toggleServiceSelection(service.service_name)} className={classNames( 'p-6 rounded-lg border-2 cursor-pointer transition-all', selected ? 'border-desert-green bg-desert-green bg-opacity-10 shadow-md text-white' : 'border-desert-stone-light bg-white hover:border-desert-green hover:shadow-sm' )} >

{service.friendly_name || service.service_name}

{service.description}

{selected ? ( ) : (
)}
) })}
)}
) const renderStep2 = () => (

Choose Map Regions

Select map region collections to download for offline use. You can always download more regions later.

{isLoadingMaps ? (
) : mapCollections && mapCollections.length > 0 ? (
{mapCollections.map((collection) => (
isOnline && !collection.all_downloaded && toggleMapCollection(collection.slug) } className={classNames( 'relative', selectedMapCollections.includes(collection.slug) && 'ring-4 ring-desert-green rounded-lg', collection.all_downloaded && 'opacity-75', !isOnline && 'opacity-50 cursor-not-allowed' )} > {selectedMapCollections.includes(collection.slug) && (
)}
))}
) : (

No map collections available at this time.

)}
) const renderStep3 = () => (

Choose ZIM Files

Select ZIM file collections for offline knowledge. You can always download more later.

{isLoadingZims ? (
) : zimCollections && zimCollections.length > 0 ? (
{zimCollections.map((collection) => (
isOnline && !collection.all_downloaded && toggleZimCollection(collection.slug) } className={classNames( 'relative', selectedZimCollections.includes(collection.slug) && 'ring-4 ring-desert-green rounded-lg', collection.all_downloaded && 'opacity-75', !isOnline && 'opacity-50 cursor-not-allowed' )} > {selectedZimCollections.includes(collection.slug) && (
)}
))}
) : (

No ZIM collections available at this time.

)}
) const renderStep4 = () => { const hasSelections = selectedServices.length > 0 || selectedMapCollections.length > 0 || selectedZimCollections.length > 0 return (

Review Your Selections

Review your choices before starting the setup process.

{!hasSelections ? ( ) : (
{selectedServices.length > 0 && (

Apps to Install ({selectedServices.length})

    {selectedServices.map((serviceName) => { const service = availableServices.find((s) => s.service_name === serviceName) return (
  • {service?.friendly_name || serviceName}
  • ) })}
)} {selectedMapCollections.length > 0 && (

Map Collections to Download ({selectedMapCollections.length})

    {selectedMapCollections.map((slug) => { const collection = mapCollections?.find((c) => c.slug === slug) return (
  • {collection?.name || slug}
  • ) })}
)} {selectedZimCollections.length > 0 && (

ZIM Collections to Download ({selectedZimCollections.length})

    {selectedZimCollections.map((slug) => { const collection = zimCollections?.find((c) => c.slug === slug) return (
  • {collection?.name || slug}
  • ) })}
)}
)}
) } return ( {!isOnline && ( )}
{renderStepIndicator()}
{currentStep === 1 && renderStep1()} {currentStep === 2 && renderStep2()} {currentStep === 3 && renderStep3()} {currentStep === 4 && renderStep4()}
{currentStep > 1 && ( Back )}

{selectedServices.length} app{selectedServices.length !== 1 && 's'},{' '} {selectedMapCollections.length} map collection {selectedMapCollections.length !== 1 && 's'}, {selectedZimCollections.length} ZIM collection{selectedZimCollections.length !== 1 && 's'} selected

router.visit('/home')} disabled={isProcessing} variant="outline" > Cancel & Go to Home {currentStep < 4 ? ( Next ) : ( Complete Setup )}
) }