stadtwerke/innungsapp/apps/admin/app/superadmin/CreateOrgForm.tsx

427 lines
30 KiB
TypeScript

'use client'
import { useActionState, useState } from 'react'
import { useRouter } from 'next/navigation'
import { createOrganization } from './actions'
import { LandingPagePreview } from './LandingPagePreview'
const initialState = { success: false, error: '' }
export function CreateOrgForm() {
const [state, formAction, isPending] = useActionState(createOrganization, initialState)
const router = useRouter()
const [step, setStep] = useState(1)
const [formData, setFormData] = useState({
name: '',
slug: '',
contactEmail: '',
adminEmail: '',
adminPassword: '',
logoUrl: '',
plan: 'pilot',
primaryColor: '#E63946',
secondaryColor: '',
landingPageTitle: '',
landingPageText: '',
landingPageHeroImage: '',
landingPageHeroOverlayOpacity: 50,
landingPageFeatures: '',
landingPageFooter: '',
appStoreUrl: '',
playStoreUrl: ''
})
const [aiContext, setAiContext] = useState('')
const [isGenerating, setIsGenerating] = useState(false)
const handleGenerateContent = async () => {
if (!formData.name || !aiContext) return
setIsGenerating(true)
try {
const res = await fetch('/api/ai/generate-landing-page', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orgName: formData.name, context: aiContext })
})
const data = await res.json()
if (data.title && data.text) {
setFormData(prev => ({
...prev,
landingPageTitle: data.title,
landingPageText: data.text
}))
}
} catch (err) {
console.error('AI generation failed', err)
} finally {
setIsGenerating(false)
}
}
const [isUploading, setIsUploading] = useState(false)
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setIsUploading(true)
const uploadFormData = new FormData()
uploadFormData.append('file', file)
try {
const res = await fetch('/api/upload', {
method: 'POST',
body: uploadFormData
})
const data = await res.json()
if (data.url) {
setFormData(prev => ({ ...prev, logoUrl: data.url }))
}
} catch (err) {
console.error('Upload failed', err)
} finally {
setIsUploading(false)
}
}
const [isHeroUploading, setIsHeroUploading] = useState(false)
const handleHeroUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setIsHeroUploading(true)
const uploadFormData = new FormData()
uploadFormData.append('file', file)
try {
const res = await fetch('/api/upload', {
method: 'POST',
body: uploadFormData
})
const data = await res.json()
if (data.url) {
setFormData(prev => ({ ...prev, landingPageHeroImage: data.url }))
}
} catch (err) {
console.error('Upload failed', err)
} finally {
setIsHeroUploading(false)
}
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
setFormData(prev => ({ ...prev, [e.target.name]: e.target.value }))
}
const nextStep = () => setStep(prev => prev + 1)
const prevStep = () => setStep(prev => prev - 1)
// Reset wizard after success
if (state.success && step !== 5) {
setStep(5)
}
return (
<div className="flex w-full h-full gap-6">
<div className="flex-[3] bg-gray-100 rounded-3xl overflow-hidden relative shadow-inner border border-gray-200 hidden lg:block">
<LandingPagePreview formData={formData} />
</div>
<div className="flex-1 bg-white p-6 sm:p-8 rounded-3xl border shadow-sm overflow-y-auto min-w-[320px] max-w-lg w-full flex flex-col">
<h2 className="text-xl font-bold text-gray-900 mb-6 font-outfit shrink-0">Neue Innung anlegen</h2>
{state.error && (
<div className="mb-6 p-4 bg-red-50 text-red-700 rounded-xl text-sm border border-red-100 animate-in fade-in slide-in-from-top-2 shrink-0">
{state.error}
</div>
)}
{/* Stepper Header (matched to screenshot) */}
<div className="flex items-center justify-start gap-2 sm:gap-4 mb-8 shrink-0 overflow-x-auto pb-2">
{[1, 2, 3, 4, 5].map((s) => (
<div key={s} className="flex items-center gap-2 sm:gap-4 shrink-0">
<div className={`shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold transition-all duration-300 ${step >= s ? 'bg-[#E63946] text-white' : 'bg-gray-100 text-gray-400'}`}>
{s}
</div>
{s < 5 && (
<div className={`h-[3px] w-8 sm:w-12 rounded-full transition-all duration-500 ${step > s ? 'bg-[#E63946]' : 'bg-gray-100'}`} />
)}
</div>
))}
</div>
<form action={formAction} className="flex-1 shrink-0 space-y-6">
{step !== 1 && (
<>
<input type="hidden" name="name" value={formData.name} />
<input type="hidden" name="slug" value={formData.slug} />
</>
)}
<input type="hidden" name="contactEmail" value={formData.contactEmail} />
<input type="hidden" name="adminEmail" value={formData.adminEmail} />
<input type="hidden" name="adminPassword" value={formData.adminPassword} />
<input type="hidden" name="logoUrl" value={formData.logoUrl} />
<input type="hidden" name="plan" value={formData.plan} />
<input type="hidden" name="primaryColor" value={formData.primaryColor} />
<input type="hidden" name="secondaryColor" value={formData.secondaryColor} />
<input type="hidden" name="landingPageTitle" value={formData.landingPageTitle} />
<input type="hidden" name="landingPageText" value={formData.landingPageText} />
<input type="hidden" name="landingPageHeroImage" value={formData.landingPageHeroImage} />
<input type="hidden" name="landingPageFeatures" value={formData.landingPageFeatures} />
<input type="hidden" name="landingPageFooter" value={formData.landingPageFooter} />
<input type="hidden" name="appStoreUrl" value={formData.appStoreUrl} />
<input type="hidden" name="playStoreUrl" value={formData.playStoreUrl} />
{step === 1 && (
<div className="space-y-5 animate-in fade-in slide-in-from-right-4 duration-300">
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Name der Innung</label>
<input type="text" name="name" required value={formData.name} onChange={handleChange} placeholder="z.B. Tischler-Innung Berlin" className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#E63946] focus:border-[#E63946] outline-none transition-all placeholder:text-gray-300" />
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Kurzbezeichnung (Slug)</label>
<input type="text" name="slug" required value={formData.slug} onChange={handleChange} placeholder="z.B. tischler-berlin" pattern="^[a-z0-9\-]+$" className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#E63946] focus:border-[#E63946] outline-none transition-all placeholder:text-gray-300" />
<p className="text-[11px] text-gray-400 mt-2 leading-relaxed">Landingpage unter: <span className="text-[#E63946] font-medium">{formData.slug ? `${formData.slug}.localhost:3032` : 'ihr-slug.localhost:3032'}</span></p>
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Planungs-Modell</label>
<select name="plan" value={formData.plan} onChange={handleChange} className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#E63946] focus:border-[#E63946] outline-none transition-all bg-white">
<option value="pilot">Pilot</option>
<option value="standard">Standard</option>
<option value="pro">Pro</option>
<option value="verband">Verband</option>
</select>
</div>
<button type="button" onClick={nextStep} disabled={!formData.name || !formData.slug} className="w-full bg-gray-900 text-white font-semibold py-3.5 px-6 rounded-xl hover:bg-black transition-all shadow-md active:scale-[0.98] disabled:opacity-50 disabled:scale-100">
Weiter zu Branding
</button>
</div>
)}
{step === 2 && (
<div className="space-y-5 animate-in fade-in slide-in-from-right-4 duration-300">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Initialer Admin (Email)</label>
<input type="email" name="adminEmail" value={formData.adminEmail} onChange={handleChange} placeholder="admin@tischler-berlin.de" className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#E63946] focus:border-[#E63946] outline-none transition-all placeholder:text-gray-300" />
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Passwort setzen</label>
<input type="text" name="adminPassword" value={formData.adminPassword} onChange={handleChange} placeholder="Sicheres Passwort" className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#E63946] focus:border-[#E63946] outline-none transition-all placeholder:text-gray-300" />
</div>
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Organisations-Logo</label>
<div className="flex items-center gap-4">
{formData.logoUrl ? (
<div className="w-14 h-14 rounded-xl border border-gray-200 overflow-hidden bg-gray-50 flex items-center justify-center p-2">
<img src={formData.logoUrl} alt="Logo" className="max-w-full max-h-full object-contain" />
</div>
) : (
<div className="w-14 h-14 rounded-xl border-2 border-dashed border-gray-200 flex items-center justify-center text-gray-300">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
</div>
)}
<label className="flex-1">
<div className={`px-4 py-3 border border-gray-200 rounded-xl w-full text-center text-sm font-semibold cursor-pointer transition-all hover:bg-gray-50 ${isUploading ? 'opacity-50' : ''}`}>
{isUploading ? 'Wird hochgeladen...' : formData.logoUrl ? 'Logo ändern' : 'Bild auswählen'}
</div>
<input type="file" onChange={handleUpload} accept="image/*" className="hidden" disabled={isUploading} />
</label>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Primärfarbe (CI)</label>
<div className="flex gap-4 items-center">
<input type="color" name="primaryColor" value={formData.primaryColor} onChange={handleChange} className="w-14 h-14 p-1 rounded-xl cursor-pointer border border-gray-200" />
<div className="flex-1">
<input type="text" value={formData.primaryColor?.toUpperCase()} readOnly className="px-4 py-3 border border-gray-200 rounded-xl w-full bg-gray-50 text-gray-500 font-mono text-sm" />
</div>
</div>
</div>
<div className="pt-4 flex gap-3">
<button type="button" onClick={prevStep} className="flex-1 bg-white text-gray-500 font-semibold py-3.5 px-6 rounded-xl border border-gray-200 hover:bg-gray-50 transition-all">
Zurück
</button>
<button type="button" onClick={nextStep} disabled={!formData.adminEmail || !formData.adminPassword} className="flex-[2] bg-gray-900 text-white font-semibold py-3.5 px-6 rounded-xl hover:bg-black transition-all shadow-md active:scale-[0.98] disabled:opacity-50">
Weiter zur Landingpage
</button>
</div>
</div>
)}
{step === 3 && (
<div className="space-y-5 animate-in fade-in slide-in-from-right-4 duration-300">
<div className="bg-blue-50/50 p-5 rounded-xl border border-blue-100">
<h3 className="text-sm font-bold text-blue-900 mb-2 font-outfit">KI Content-Erstellung</h3>
<p className="text-xs text-blue-700 leading-relaxed mb-4">
Beschreiben Sie in wenigen Stichpunkten, worauf die Innung fokussiert ist (Region, Tradition, Ausbildung, etc.). Die KI generiert daraus eine moderne Landingpage.
</p>
<textarea
value={aiContext}
onChange={(e) => setAiContext(e.target.value)}
placeholder="z.B. Kreishandwerkerschaft Niederrhein, Fokus auf Ausbildung und Digitalisierung im Handwerk..."
className="w-full px-4 py-3 border border-blue-200 bg-white rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all placeholder:text-gray-400 text-sm min-h-[80px]"
/>
<button
type="button"
onClick={handleGenerateContent}
disabled={isGenerating || !aiContext}
className="mt-3 w-full bg-blue-600 text-white font-semibold py-2.5 px-6 rounded-lg hover:bg-blue-700 transition-all shadow-sm disabled:opacity-50 flex items-center justify-center gap-2 text-sm"
>
{isGenerating ? (
<>
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Generieren...
</>
) : '✨ Content generieren'}
</button>
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Überschrift</label>
<input type="text" name="landingPageTitle" value={formData.landingPageTitle} onChange={handleChange} placeholder="Zukunft durch Handwerk" className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#E63946] focus:border-[#E63946] outline-none transition-all placeholder:text-gray-300 font-bold" />
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Einleitungstext</label>
<textarea name="landingPageText" value={formData.landingPageText} onChange={(e) => setFormData(prev => ({ ...prev, landingPageText: e.target.value }))} placeholder="Wir sind..." className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#E63946] focus:border-[#E63946] outline-none transition-all placeholder:text-gray-300 min-h-[100px] text-sm leading-relaxed" />
</div>
<div className="pt-4 flex gap-3">
<button type="button" onClick={prevStep} className="flex-1 bg-white text-gray-500 font-semibold py-3.5 px-6 rounded-xl border border-gray-200 hover:bg-gray-50 transition-all">
Zurück
</button>
<button type="button" onClick={nextStep} className="flex-[2] bg-gray-900 text-white font-semibold py-3.5 px-6 rounded-xl hover:bg-black transition-all shadow-md active:scale-[0.98]">
Weiter zu Erweitert
</button>
</div>
</div>
)}
{step === 4 && (
<div className="space-y-5 animate-in fade-in slide-in-from-right-4 duration-300">
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Hero-Titelbild</label>
<div className="flex items-center gap-4">
{formData.landingPageHeroImage ? (
<div className="w-24 h-14 rounded-xl border border-gray-200 overflow-hidden bg-gray-50 flex items-center justify-center p-0">
<img src={formData.landingPageHeroImage} alt="Hero" className="max-w-full max-h-full object-cover" />
</div>
) : (
<div className="w-24 h-14 rounded-xl border-2 border-dashed border-gray-200 flex items-center justify-center text-gray-300">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
</div>
)}
<label className="flex-1">
<div className={`px-4 py-3 border border-gray-200 rounded-xl w-full text-center text-sm font-semibold cursor-pointer transition-all hover:bg-gray-50 ${isHeroUploading ? 'opacity-50' : ''}`}>
{isHeroUploading ? 'Wird hochgeladen...' : formData.landingPageHeroImage ? 'Bild ändern' : 'Bild auswählen'}
</div>
<input type="file" onChange={handleHeroUpload} accept="image/*" className="hidden" disabled={isHeroUploading} />
</label>
</div>
</div>
{formData.landingPageHeroImage && (
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-1">
Hero-Deckkraft (Opacity: {formData.landingPageHeroOverlayOpacity}%)
</label>
<input
type="range"
name="landingPageHeroOverlayOpacity"
min="0"
max="100"
value={formData.landingPageHeroOverlayOpacity}
onChange={handleChange}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-[#E63946]"
/>
<p className="text-xs text-gray-400 mt-1">Bestimmt, wie stark das Bild abgedunkelt/aufgehellt wird, um den Text lesbar zu machen.</p>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Sekundärfarbe (Optional)</label>
<div className="flex gap-4 items-center">
<input type="color" name="secondaryColor" value={formData.secondaryColor || '#ffffff'} onChange={handleChange} className="w-14 h-14 p-1 rounded-xl cursor-pointer border border-gray-200" />
<div className="flex-1">
<input type="text" name="secondaryColor" value={formData.secondaryColor?.toUpperCase()} onChange={handleChange} placeholder="#FFFFFF" className="px-4 py-3 border border-gray-200 rounded-xl w-full bg-white text-gray-700 font-mono text-sm" />
</div>
</div>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Vorteile / Features</label>
<textarea name="landingPageFeatures" value={formData.landingPageFeatures} onChange={handleChange} placeholder="Ein Benefit pro Zeile..." className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#E63946] focus:border-[#E63946] outline-none transition-all placeholder:text-gray-300 min-h-[120px] text-sm leading-relaxed" />
<p className="text-xs text-gray-400 mt-2">Bitte geben Sie pro Zeile einen Vorteil ein. Diese werden als Checkliste auf der Landingpage angezeigt.</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">App Store URL</label>
<input type="url" name="appStoreUrl" value={formData.appStoreUrl} onChange={handleChange} placeholder="https://apps.apple.com/..." className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#E63946] focus:border-[#E63946] outline-none transition-all placeholder:text-gray-300 text-sm" />
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Google Play URL</label>
<input type="url" name="playStoreUrl" value={formData.playStoreUrl} onChange={handleChange} placeholder="https://play.google.com/..." className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#E63946] focus:border-[#E63946] outline-none transition-all placeholder:text-gray-300 text-sm" />
</div>
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2">Footer-Text (Impressum, etc.)</label>
<textarea name="landingPageFooter" value={formData.landingPageFooter} onChange={handleChange} placeholder="Zusätzliche Infos für den Footer..." className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#E63946] focus:border-[#E63946] outline-none transition-all placeholder:text-gray-300 min-h-[80px] text-sm leading-relaxed" />
</div>
<div className="pt-4 flex gap-3">
<button type="button" onClick={prevStep} className="flex-1 bg-white text-gray-500 font-semibold py-3.5 px-6 rounded-xl border border-gray-200 hover:bg-gray-50 transition-all">
Zurück
</button>
<button type="submit" disabled={isPending} className="flex-[2] bg-[#E63946] text-white font-semibold py-3.5 px-6 rounded-xl hover:bg-[#D62839] transition-all shadow-md shadow-red-100 active:scale-[0.98] disabled:opacity-50 flex justify-center items-center gap-2">
{isPending ? 'Wird erstellt...' : 'Innung anlegen'}
</button>
</div>
</div>
)}
{step === 5 && (
<div className="text-center animate-in fade-in zoom-in-95 duration-700 py-4">
<div className="w-24 h-24 bg-[#E8F5E9] text-[#2E7D32] rounded-full flex items-center justify-center mx-auto mb-8 animate-in zoom-in-50 duration-500 delay-150">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2.5} stroke="currentColor" className="w-10 h-10">
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">Innung erfolgreich angelegt!</h3>
<p className="text-gray-500 text-sm mb-10">Die Datenumgebung sowie die Subdomain<br />wurden eingerichtet.</p>
<div className="bg-[#F8FEFB] p-6 rounded-2xl border border-[#E1F5EA] text-left mb-8">
<p className="text-[10px] font-bold text-[#8CAB99] uppercase tracking-[0.15em] mb-4">Ihre neue Landingpage (Localhost) / Subdomain</p>
<a href={`http://${formData.slug}.localhost:3032`} target="_blank" rel="noreferrer" className="text-[#E63946] font-bold text-lg hover:underline block break-all">
{formData.slug}.localhost:3032
</a>
</div>
<button type="button" onClick={() => {
router.push('/superadmin')
}} className="w-full bg-[#F3F4F6] text-[#4B5563] font-bold py-4 px-6 rounded-2xl hover:bg-gray-200 transition-all active:scale-[0.98]">
Zurück zur Übersicht
</button>
</div>
)}
</form>
</div>
</div>
)
}