import { useState, useEffect } from 'react' import { createRoot } from 'react-dom/client' import { Button } from '@public/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@public/components/ui/card' import { Input } from '@public/components/ui/input' import { Label } from '@public/components/ui/label' import { Globe, Upload, CheckCircle2, Loader2, AlertCircle } from 'lucide-react' import Layout from '@public/layouts' type OnboardingStep = 'domain' | 'upload' | 'complete' function Onboarding() { const [step, setStep] = useState('domain') const [handle, setHandle] = useState('') const [isCheckingAvailability, setIsCheckingAvailability] = useState(false) const [isAvailable, setIsAvailable] = useState(null) const [domain, setDomain] = useState('') const [isClaimingDomain, setIsClaimingDomain] = useState(false) const [claimedDomain, setClaimedDomain] = useState('') const [siteName, setSiteName] = useState('') const [selectedFiles, setSelectedFiles] = useState(null) const [isUploading, setIsUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState('') const [skippedFiles, setSkippedFiles] = useState>([]) const [uploadedCount, setUploadedCount] = useState(0) // Check domain availability as user types useEffect(() => { if (!handle || handle.length < 3) { setIsAvailable(null) setDomain('') return } const timeoutId = setTimeout(async () => { setIsCheckingAvailability(true) try { const response = await fetch( `/api/domain/check?handle=${encodeURIComponent(handle)}` ) const data = await response.json() setIsAvailable(data.available) setDomain(data.domain || '') } catch (err) { console.error('Error checking availability:', err) setIsAvailable(false) } finally { setIsCheckingAvailability(false) } }, 500) return () => clearTimeout(timeoutId) }, [handle]) const handleClaimDomain = async () => { if (!handle || !isAvailable) return setIsClaimingDomain(true) try { const response = await fetch('/api/domain/claim', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ handle }) }) const data = await response.json() if (data.success) { setClaimedDomain(data.domain) setStep('upload') } else { throw new Error(data.error || 'Failed to claim domain') } } catch (err) { console.error('Error claiming domain:', err) const errorMessage = err instanceof Error ? err.message : 'Unknown error' // Handle "Already claimed" error - redirect to editor if (errorMessage.includes('Already claimed')) { alert('You have already claimed a wisp.place subdomain. Redirecting to editor...') window.location.href = '/editor' } else { alert(`Failed to claim domain: ${errorMessage}`) } } finally { setIsClaimingDomain(false) } } const handleFileSelect = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { setSelectedFiles(e.target.files) } } const handleUpload = async () => { if (!siteName) { alert('Please enter a site name') return } setIsUploading(true) setUploadProgress('Preparing files...') try { const formData = new FormData() formData.append('siteName', siteName) if (selectedFiles) { for (let i = 0; i < selectedFiles.length; i++) { formData.append('files', selectedFiles[i]) } } setUploadProgress('Uploading to AT Protocol...') const response = await fetch('/wisp/upload-files', { method: 'POST', body: formData }) const data = await response.json() if (data.success) { setUploadProgress('Upload complete!') setSkippedFiles(data.skippedFiles || []) setUploadedCount(data.uploadedCount || data.fileCount || 0) // If there are skipped files, show them briefly before redirecting if (data.skippedFiles && data.skippedFiles.length > 0) { setTimeout(() => { window.location.href = `https://${claimedDomain}` }, 3000) // Give more time to see skipped files } else { setTimeout(() => { window.location.href = `https://${claimedDomain}` }, 1500) } } else { throw new Error(data.error || 'Upload failed') } } catch (err) { console.error('Upload error:', err) alert( `Upload failed: ${err instanceof Error ? err.message : 'Unknown error'}` ) setIsUploading(false) setUploadProgress('') } } const handleSkipUpload = () => { // Redirect to editor without uploading window.location.href = '/editor' } return (
{/* Header */}
wisp.place
{/* Progress indicator */}
{step === 'domain' ? ( '1' ) : ( )}
{step === 'complete' ? ( ) : ( '2' )}

{step === 'domain' && 'Claim Your Free Domain'} {step === 'upload' && 'Deploy Your First Site'} {step === 'complete' && 'All Set!'}

{step === 'domain' && 'Choose a subdomain on wisp.place'} {step === 'upload' && 'Upload your site or start with an empty one'} {step === 'complete' && 'Redirecting to your site...'}

{/* Domain registration step */} {step === 'domain' && ( Choose Your Domain Pick a unique handle for your free *.wisp.place subdomain
setHandle( e.target.value .toLowerCase() .replace(/[^a-z0-9-]/g, '') ) } className="pr-10" /> {isCheckingAvailability && ( )} {!isCheckingAvailability && isAvailable !== null && (
{isAvailable ? '✓' : '✗'}
)}
{domain && (

Your domain will be:{' '} {domain}

)} {isAvailable === false && handle.length >= 3 && (

This handle is not available or invalid

)}
)} {/* Upload step */} {step === 'upload' && ( Deploy Your Site Upload your static site files or start with an empty site (you can upload later)
Domain claimed: {claimedDomain}
setSiteName(e.target.value)} />

A unique identifier for this site in your account

{selectedFiles && selectedFiles.length > 0 && (

{selectedFiles.length} files selected

)}

Supported: HTML, CSS, JS, images, fonts, and more

Limits: 100MB per file, 300MB total

{uploadProgress && (
{uploadProgress}
{skippedFiles.length > 0 && (
{skippedFiles.length} file{skippedFiles.length > 1 ? 's' : ''} skipped {uploadedCount > 0 && ( ({uploadedCount} uploaded successfully) )}
{skippedFiles.slice(0, 5).map((file, idx) => (
{file.name} - {file.reason}
))} {skippedFiles.length > 5 && (
...and {skippedFiles.length - 5} more
)}
)}
)}
)}
) } const root = createRoot(document.getElementById('elysia')!) root.render( )