feat: easy setup wizard

This commit is contained in:
Jake Turner 2026-01-15 19:03:05 +00:00 committed by Jake Turner
parent 6500599c6d
commit 4b74118fd9
4 changed files with 22 additions and 35 deletions

View File

@ -27,7 +27,7 @@ const InstallActivityFeed: React.FC<InstallActivityFeedProps> = ({ activity, cla
return ( return (
<div className={classNames('bg-white shadow-sm rounded-lg p-6', className)}> <div className={classNames('bg-white shadow-sm rounded-lg p-6', className)}>
{withHeader && <h2 className="text-lg font-semibold text-gray-900">Installation Activity</h2>} {withHeader && <h2 className="text-lg font-semibold text-gray-900">Installation Activity</h2>}
<ul role="list" className="mt-6 space-y-6 text-desert-green"> <ul role="list" className={classNames("space-y-6 text-desert-green", withHeader ? 'mt-6' : '')}>
{activity.map((activityItem, activityItemIdx) => ( {activity.map((activityItem, activityItemIdx) => (
<li key={activityItem.timestamp} className="relative flex gap-x-4"> <li key={activityItem.timestamp} className="relative flex gap-x-4">
<div <div

View File

@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
import api from '~/lib/api'; import api from '~/lib/api';
const useInternetStatus = () => { const useInternetStatus = () => {
const [isOnline, setIsOnline] = useState<boolean>(false); const [isOnline, setIsOnline] = useState<boolean>(true); // Initialize true to avoid "offline" flicker on load
const { data } = useQuery<boolean>({ const { data } = useQuery<boolean>({
queryKey: ['internetStatus'], queryKey: ['internetStatus'],
queryFn: async () => (await api.getInternetStatus()) ?? false, queryFn: async () => (await api.getInternetStatus()) ?? false,

View File

@ -1,6 +1,6 @@
import { Head, router } from '@inertiajs/react' import { Head, router } from '@inertiajs/react'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { useState, useMemo } from 'react' import { useState } from 'react'
import AppLayout from '~/layouts/AppLayout' import AppLayout from '~/layouts/AppLayout'
import StyledButton from '~/components/StyledButton' import StyledButton from '~/components/StyledButton'
import api from '~/lib/api' import api from '~/lib/api'
@ -42,9 +42,9 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}) })
const availableServices = useMemo(() => { const availableServices = props.system.services.filter(
return props.system.services.filter((service) => !service.installed) (service) => !service.installed && service.installation_status !== 'installing'
}, [props.system.services]) )
const toggleServiceSelection = (serviceName: string) => { const toggleServiceSelection = (serviceName: string) => {
setSelectedServices((prev) => setSelectedServices((prev) =>
@ -97,26 +97,23 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
try { 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 // 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)) const installPromises = selectedServices.map((serviceName) => api.installService(serviceName))
// await Promise.all(installPromises) await Promise.all(installPromises)
// const downloadPromises = [ const downloadPromises = [
// ...selectedMapCollections.map((slug) => api.downloadMapCollection(slug)), ...selectedMapCollections.map((slug) => api.downloadMapCollection(slug)),
// ...selectedZimCollections.map((slug) => api.downloadZimCollection(slug)), ...selectedZimCollections.map((slug) => api.downloadZimCollection(slug)),
// ] ]
// await Promise.all(downloadPromises) await Promise.all(downloadPromises)
addNotification({ addNotification({
type: 'success', type: 'success',
message: 'Setup wizard completed! Your selections are being processed.', message: 'Setup wizard completed! Your selections are being processed.',
}) })
// Wait a moment then redirect to completion page to show progress router.visit('/easy-setup/complete')
setTimeout(() => {
router.visit('/easy-setup/complete')
}, 2000)
} catch (error) { } catch (error) {
console.error('Error during setup:', error) console.error('Error during setup:', error)
addNotification({ addNotification({
@ -226,26 +223,17 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{availableServices.map((service) => { {availableServices.map((service) => {
const selectedOrInstalled = const selected = selectedServices.includes(service.service_name)
selectedServices.includes(service.service_name) ||
service.installed ||
service.installation_status === 'installing'
const installedOrInstalling =
service.installed || service.installation_status === 'installing'
return ( return (
<div <div
key={service.id} key={service.id}
onClick={() => onClick={() => toggleServiceSelection(service.service_name)}
!installedOrInstalling && toggleServiceSelection(service.service_name)
}
className={classNames( className={classNames(
'p-6 rounded-lg border-2 cursor-pointer transition-all', 'p-6 rounded-lg border-2 cursor-pointer transition-all',
selectedOrInstalled selected
? 'border-desert-green bg-desert-green bg-opacity-10 shadow-md text-white' ? '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', : 'border-desert-stone-light bg-white hover:border-desert-green hover:shadow-sm'
installedOrInstalling ? 'opacity-50 cursor-not-allowed' : ''
)} )}
> >
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
@ -256,7 +244,7 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
<p <p
className={classNames( className={classNames(
'text-sm mt-1', 'text-sm mt-1',
selectedOrInstalled ? 'text-white' : 'text-gray-600' selected ? 'text-white' : 'text-gray-600'
)} )}
> >
{service.description} {service.description}
@ -265,12 +253,10 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
<div <div
className={classNames( className={classNames(
'ml-4 w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all', 'ml-4 w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all',
selectedOrInstalled selected ? 'border-desert-green bg-desert-green' : 'border-desert-stone'
? 'border-desert-green bg-desert-green'
: 'border-desert-stone'
)} )}
> >
{selectedOrInstalled ? ( {selected ? (
<IconCheck size={20} className="text-white" /> <IconCheck size={20} className="text-white" />
) : ( ) : (
<div className="w-5 h-5 rounded-full bg-transparent" /> <div className="w-5 h-5 rounded-full bg-transparent" />

View File

@ -230,6 +230,7 @@ export default function ZimRemoteExplorer() {
key={collection.slug} key={collection.slug}
collection={collection} collection={collection}
onClick={(collection) => confirmDownload(collection)} onClick={(collection) => confirmDownload(collection)}
size='large'
/> />
))} ))}
</div> </div>