'use client'; import Link from 'next/link'; import { useMemo, useState } from 'react'; import '../admin/admin.css'; import './install.css'; interface Album { id: string; albumName: string; assetCount: number; description: string; } interface Props { initialApiUrl: string; setupToken: string; } const STEPS = ['Connect', 'Albums', 'Site', 'Finish'] as const; const THEME_OPTIONS = [ { value: 'studio', label: 'Studio', description: 'Classic portfolio, red accent' }, { value: 'studio-modern', label: 'Studio Modern', description: 'Clean with soft corners' }, { value: 'minimal', label: 'Minimal', description: 'Bare-bones, monochrome' }, { value: 'editorial', label: 'Editorial', description: 'Magazine-style serif' }, { value: 'classic', label: 'Classic', description: 'Warm gold, traditional' }, { value: 'noir', label: 'Noir', description: 'Dark, high-contrast' }, { value: 'monograph', label: 'Monograph', description: 'Typographic, text-first' }, ]; export function InstallWizard({ initialApiUrl, setupToken }: Props) { const [step, setStep] = useState(0); // Step 1 — connection const [apiUrl, setApiUrl] = useState(initialApiUrl); const [apiKey, setApiKey] = useState(''); const [testing, setTesting] = useState(false); const [testError, setTestError] = useState(''); // Step 2 — album selection (optional) const [albums, setAlbums] = useState([]); const [selected, setSelected] = useState>({}); const [search, setSearch] = useState(''); const [loaded, setLoaded] = useState(false); // Step 3 — site settings const [siteTitle, setSiteTitle] = useState(''); const [siteSubtitle, setSiteSubtitle] = useState(''); const [theme, setTheme] = useState('studio'); const [adminPassword, setAdminPassword] = useState(''); const [adminConfirm, setAdminConfirm] = useState(''); const [passwordError, setPasswordError] = useState(''); // Step 4 — finish const [installing, setInstalling] = useState(false); const [installError, setInstallError] = useState(''); const [done, setDone] = useState(false); const selectedCount = Object.values(selected).filter(Boolean).length; const filteredAlbums = useMemo(() => { const q = search.trim().toLowerCase(); if (!q) return albums; return albums.filter((a) => a.albumName.toLowerCase().includes(q)); }, [albums, search]); async function testConnection() { setTesting(true); setTestError(''); try { const res = await fetch('/api/install/albums', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-setup-token': setupToken }, body: JSON.stringify({ apiUrl: apiUrl.trim(), apiKey: apiKey.trim() }), }); const data = await res.json(); if (!res.ok) { setTestError(data.error || 'Could not connect to Immich'); return; } setAlbums(data.albums || []); setLoaded(true); setStep(1); } catch { setTestError('Could not reach the install service'); } finally { setTesting(false); } } function toggleAlbum(id: string) { setSelected((prev) => ({ ...prev, [id]: !prev[id] })); } function validateSiteStep(): boolean { if (adminPassword && adminPassword !== adminConfirm) { setPasswordError('Passwords do not match'); return false; } setPasswordError(''); return true; } async function runInstall() { setInstalling(true); setInstallError(''); try { const res = await fetch('/api/install', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-setup-token': setupToken }, body: JSON.stringify({ apiUrl: apiUrl.trim(), apiKey: apiKey.trim(), siteTitle, siteSubtitle, theme, albums: Object.keys(selected).filter((id) => selected[id]), ...(adminPassword ? { adminPassword } : {}), }), }); const data = await res.json(); if (!res.ok) { setInstallError(data.error || 'Install failed'); return; } setDone(true); } catch { setInstallError('Could not reach the install service'); } finally { setInstalling(false); } } if (done) { return (
Immich Folio

Install complete

Your portfolio is ready. Add or rearrange albums any time from the admin panel.

{/* install-body carries the card's padding — every other step renders inside it, and skipping it here left the buttons flush against the bottom edge. */}
Visit your gallery {adminPassword && ( Open admin panel )}
); } return (
Immich Folio

Install wizard

Connect your Immich server, choose what to show, and publish your portfolio.

    {STEPS.map((label, i) => (
  1. {i < step ? '✓' : i + 1} {label}
  2. ))}
{step === 0 && (
{ e.preventDefault(); testConnection(); }} >
setApiUrl(e.target.value)} placeholder="https://photos.example.com" required autoFocus disabled={testing} />

The base URL of your Immich instance (no /api). Example:{' '} https://photos.example.com

setApiKey(e.target.value)} placeholder="Your Immich API key" required disabled={testing} />

Create one in Immich under Admin Settings → API Keys.

{testError &&
{testError}
}
)} {step === 1 && (

Pick albums to publish. This step is optional — you can add albums later from the admin panel.

{albums.length === 0 ? (

{loaded ? 'No shared albums found on this Immich server.' : 'Albums could not be loaded.'}

Immich only lists albums that have been shared. You can still continue and add them later.

) : ( <>
setSearch(e.target.value)} placeholder="Search albums…" aria-label="Search albums" />
    {filteredAlbums.map((album) => (
  • ))} {filteredAlbums.length === 0 && (
  • No albums match “{search}”.
  • )}
)}
)} {step === 2 && (
{ e.preventDefault(); if (validateSiteStep()) setStep(3); }} >
setSiteTitle(e.target.value)} placeholder="My Photography" autoFocus />
setSiteSubtitle(e.target.value)} placeholder="A visual journal" />

{THEME_OPTIONS.find((t) => t.value === theme)?.description}

Optional — admin panel
setAdminPassword(e.target.value)} placeholder="Leave empty to disable the admin panel" />
{adminPassword && (
setAdminConfirm(e.target.value)} placeholder="Repeat the password" />
)} {passwordError &&
{passwordError}
}
)} {step === 3 && (
Immich server
{apiUrl}
Albums
{selectedCount > 0 ? `${selectedCount} selected` : 'None — you can add them later'}
Site title
{siteTitle || 'Gallery'}
Theme
{THEME_OPTIONS.find((t) => t.value === theme)?.label}
Admin panel
{adminPassword ? 'Enabled' : 'Disabled'}
{installError &&
{installError}
}
)}
); }