log
This commit is contained in:
@@ -1,89 +1,89 @@
|
||||
'use client'
|
||||
|
||||
import { useActionState, useState } from 'react'
|
||||
import { createAdmin } from '../../actions'
|
||||
|
||||
export function CreateAdminForm({ orgId }: { orgId: string }) {
|
||||
const [state, action, isPending] = useActionState(createAdmin, { success: false, error: '' })
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
|
||||
if (!showForm) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setShowForm(true)}
|
||||
className="w-full py-2 border-2 border-dashed border-gray-200 rounded-lg text-sm text-gray-500 hover:border-brand-500 hover:text-brand-500 transition-all font-medium"
|
||||
>
|
||||
+ Administrator hinzufügen
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 border rounded-xl p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-gray-900">Neuen Admin anlegen</h3>
|
||||
<button
|
||||
onClick={() => setShowForm(false)}
|
||||
className="text-xs text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form action={action} className="space-y-3">
|
||||
<input type="hidden" name="orgId" value={orgId} />
|
||||
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Name</label>
|
||||
<input
|
||||
name="name"
|
||||
required
|
||||
placeholder="z.B. Max Mustermann"
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">E-Mail</label>
|
||||
<input
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="admin@beispiel.de"
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Passwort</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
name="password"
|
||||
type="text"
|
||||
required
|
||||
defaultValue={Math.random().toString(36).slice(-10)}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm font-mono focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
<p className="text-[10px] text-gray-400 mt-1">Das Passwort muss dem Admin manuell mitgeteilt werden.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{state.error && (
|
||||
<p className="text-xs text-red-600 bg-red-50 p-2 rounded">{state.error}</p>
|
||||
)}
|
||||
|
||||
{state.success && (
|
||||
<p className="text-xs text-green-600 bg-green-50 p-2 rounded">Administrator erfolgreich angelegt.</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full bg-gray-900 text-white py-2 rounded-lg text-sm font-medium hover:bg-gray-800 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isPending ? 'Wird angelegt...' : 'Admin anlegen'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
'use client'
|
||||
|
||||
import { useActionState, useState } from 'react'
|
||||
import { createAdmin } from '../../actions'
|
||||
|
||||
export function CreateAdminForm({ orgId }: { orgId: string }) {
|
||||
const [state, action, isPending] = useActionState(createAdmin, { success: false, error: '' })
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
|
||||
if (!showForm) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setShowForm(true)}
|
||||
className="w-full py-2 border-2 border-dashed border-gray-200 rounded-lg text-sm text-gray-500 hover:border-brand-500 hover:text-brand-500 transition-all font-medium"
|
||||
>
|
||||
+ Administrator hinzufügen
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 border rounded-xl p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-gray-900">Neuen Admin anlegen</h3>
|
||||
<button
|
||||
onClick={() => setShowForm(false)}
|
||||
className="text-xs text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form action={action} className="space-y-3">
|
||||
<input type="hidden" name="orgId" value={orgId} />
|
||||
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Name</label>
|
||||
<input
|
||||
name="name"
|
||||
required
|
||||
placeholder="z.B. Max Mustermann"
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">E-Mail</label>
|
||||
<input
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="admin@beispiel.de"
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Passwort</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
name="password"
|
||||
type="text"
|
||||
required
|
||||
defaultValue={Math.random().toString(36).slice(-10)}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm font-mono focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
<p className="text-[10px] text-gray-400 mt-1">Das Passwort muss dem Admin manuell mitgeteilt werden.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{state.error && (
|
||||
<p className="text-xs text-red-600 bg-red-50 p-2 rounded">{state.error}</p>
|
||||
)}
|
||||
|
||||
{state.success && (
|
||||
<p className="text-xs text-green-600 bg-green-50 p-2 rounded">Administrator erfolgreich angelegt.</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full bg-gray-900 text-white py-2 rounded-lg text-sm font-medium hover:bg-gray-800 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isPending ? 'Wird angelegt...' : 'Admin anlegen'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,94 +1,94 @@
|
||||
'use client'
|
||||
|
||||
'use client'
|
||||
|
||||
import { useActionState } from 'react'
|
||||
import { createMember } from '../../actions'
|
||||
|
||||
const initialState = {
|
||||
success: false,
|
||||
error: '',
|
||||
}
|
||||
|
||||
export function CreateMemberForm({ orgId }: { orgId: string }) {
|
||||
const [state, action, isPending] = useActionState(createMember, initialState)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wider">Mitglied manuell hinzufügen</h3>
|
||||
<form action={action} className="space-y-3">
|
||||
<input type="hidden" name="orgId" value={orgId} />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Name (Ansprechpartner)</label>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Anrede Vorname Nachname"
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">E-Mail</label>
|
||||
<input
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="email@beispiel.de"
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Betrieb</label>
|
||||
<input
|
||||
name="betrieb"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Name des Betriebs"
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Sparte</label>
|
||||
<input
|
||||
name="sparte"
|
||||
type="text"
|
||||
required
|
||||
placeholder="z.B. Sanitär"
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Ort</label>
|
||||
<input
|
||||
name="ort"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Stadt"
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{state.error && (
|
||||
<p className="text-xs text-red-600 bg-red-50 p-2 rounded">{state.error}</p>
|
||||
)}
|
||||
|
||||
{state.success && (
|
||||
<p className="text-xs text-green-600 bg-green-50 p-2 rounded">Mitglied erfolgreich angelegt.</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full bg-gray-900 text-white py-2 rounded-lg text-sm font-medium hover:bg-gray-800 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isPending ? 'Wird angelegt...' : 'Mitglied anlegen'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
success: false,
|
||||
error: '',
|
||||
}
|
||||
|
||||
export function CreateMemberForm({ orgId }: { orgId: string }) {
|
||||
const [state, action, isPending] = useActionState(createMember, initialState)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wider">Mitglied manuell hinzufügen</h3>
|
||||
<form action={action} className="space-y-3">
|
||||
<input type="hidden" name="orgId" value={orgId} />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Name (Ansprechpartner)</label>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Anrede Vorname Nachname"
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">E-Mail</label>
|
||||
<input
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="email@beispiel.de"
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Betrieb</label>
|
||||
<input
|
||||
name="betrieb"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Name des Betriebs"
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Sparte</label>
|
||||
<input
|
||||
name="sparte"
|
||||
type="text"
|
||||
required
|
||||
placeholder="z.B. Sanitär"
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Ort</label>
|
||||
<input
|
||||
name="ort"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Stadt"
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{state.error && (
|
||||
<p className="text-xs text-red-600 bg-red-50 p-2 rounded">{state.error}</p>
|
||||
)}
|
||||
|
||||
{state.success && (
|
||||
<p className="text-xs text-green-600 bg-green-50 p-2 rounded">Mitglied erfolgreich angelegt.</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full bg-gray-900 text-white py-2 rounded-lg text-sm font-medium hover:bg-gray-800 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isPending ? 'Wird angelegt...' : 'Mitglied anlegen'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,351 +1,351 @@
|
||||
'use client'
|
||||
|
||||
import { useActionState, useState } from 'react'
|
||||
import { updateOrganization } from '../../actions'
|
||||
|
||||
function jsonToText(value: unknown): string {
|
||||
if (value == null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => (typeof item === 'string' ? item : JSON.stringify(item)))
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
interface Props {
|
||||
org: {
|
||||
id: string
|
||||
name: string
|
||||
plan: string
|
||||
contactEmail: string | null
|
||||
logoUrl: string | null
|
||||
primaryColor: string | null
|
||||
secondaryColor: string | null
|
||||
landingPageTitle: string | null
|
||||
landingPageText: string | null
|
||||
landingPageSectionTitle: string | null
|
||||
landingPageButtonText: string | null
|
||||
landingPageHeroImage: string | null
|
||||
landingPageHeroOverlayOpacity: number | null
|
||||
landingPageFeatures: unknown
|
||||
landingPageFooter: unknown
|
||||
appStoreUrl: string | null
|
||||
playStoreUrl: string | null
|
||||
}
|
||||
}
|
||||
|
||||
const initialState = { success: false, error: '' }
|
||||
|
||||
export function EditOrgForm({ org }: Props) {
|
||||
const boundAction = updateOrganization.bind(null, org.id)
|
||||
const [state, formAction, isPending] = useActionState(boundAction, initialState)
|
||||
const [logoUrl, setLogoUrl] = useState(org.logoUrl || '')
|
||||
const [heroImageUrl, setHeroImageUrl] = useState(org.landingPageHeroImage || '')
|
||||
const [isUploading, setIsUploading] = useState<{ logo?: boolean; hero?: boolean }>({})
|
||||
const [themeColor, setThemeColor] = useState(org.primaryColor || '#E63946')
|
||||
const [secondaryColor, setSecondaryColor] = useState(org.secondaryColor || '#FFFFFF')
|
||||
|
||||
const initialFeatures = jsonToText(org.landingPageFeatures)
|
||||
const initialFooter = jsonToText(org.landingPageFooter)
|
||||
|
||||
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>, type: 'logo' | 'hero') => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
setIsUploading(prev => ({ ...prev, [type]: 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) {
|
||||
if (type === 'logo') setLogoUrl(data.url)
|
||||
if (type === 'hero') setHeroImageUrl(data.url)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload failed', err)
|
||||
} finally {
|
||||
setIsUploading(prev => ({ ...prev, [type]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border p-6">
|
||||
<h2 className="text-base font-semibold text-gray-900 mb-4">Innung bearbeiten</h2>
|
||||
|
||||
{state.success && (
|
||||
<div className="mb-4 p-3 bg-green-50 text-green-700 rounded-lg text-sm">Änderungen gespeichert.</div>
|
||||
)}
|
||||
{state.error && (
|
||||
<div className="mb-4 p-3 bg-red-50 text-red-700 rounded-lg text-sm">{state.error}</div>
|
||||
)}
|
||||
|
||||
<form action={formAction} className="space-y-6">
|
||||
{/* BASISDATEN */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900 border-b pb-2">Basisdaten</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Name der Innung</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
defaultValue={org.name}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Plan</label>
|
||||
<select
|
||||
name="plan"
|
||||
defaultValue={org.plan}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500 bg-white"
|
||||
>
|
||||
<option value="pilot">Pilot</option>
|
||||
<option value="standard">Standard</option>
|
||||
<option value="pro">Pro</option>
|
||||
<option value="verband">Verband</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Kontakt E-Mail</label>
|
||||
<input
|
||||
type="email"
|
||||
name="contactEmail"
|
||||
defaultValue={org.contactEmail ?? ''}
|
||||
placeholder="info@innung.de"
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BRANDING */}
|
||||
<div className="space-y-4 pt-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900 border-b pb-2">Branding</h3>
|
||||
|
||||
<input type="hidden" name="logoUrl" value={logoUrl} />
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Logo</label>
|
||||
<div className="flex items-center gap-3">
|
||||
{logoUrl ? (
|
||||
<div className="w-10 h-10 rounded border bg-gray-50 flex items-center justify-center p-1">
|
||||
<img src={logoUrl} alt="Logo" className="max-w-full max-h-full object-contain" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded border-2 border-dashed 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-5 h-5">
|
||||
<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 cursor-pointer">
|
||||
<div className={`px-3 py-2 border rounded-lg text-sm text-center font-medium hover:bg-gray-50 transition-colors ${isUploading.logo ? 'opacity-50' : ''}`}>
|
||||
{isUploading.logo ? 'Wird hochgeladen...' : 'Logo ändern'}
|
||||
</div>
|
||||
<input type="file" onChange={(e) => handleUpload(e, 'logo')} accept="image/*" className="hidden" disabled={isUploading.logo} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Primärfarbe</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="color"
|
||||
name="primaryColor"
|
||||
value={themeColor}
|
||||
onChange={(e) => setThemeColor(e.target.value)}
|
||||
className="h-9 w-12 p-1 border rounded cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={themeColor}
|
||||
onChange={(e) => setThemeColor(e.target.value)}
|
||||
className="flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 font-mono text-sm"
|
||||
pattern="^#([A-Fa-f0-9]{6})$"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Sekundärfarbe</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="color"
|
||||
name="secondaryColor"
|
||||
value={secondaryColor}
|
||||
onChange={(e) => setSecondaryColor(e.target.value)}
|
||||
className="h-9 w-12 p-1 border rounded cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={secondaryColor}
|
||||
onChange={(e) => setSecondaryColor(e.target.value)}
|
||||
className="flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 font-mono text-sm"
|
||||
pattern="^#([A-Fa-f0-9]{6})$"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* LANDING PAGE */}
|
||||
<div className="space-y-4 pt-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900 border-b pb-2">Landing Page</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Hero Titel</label>
|
||||
<input
|
||||
type="text"
|
||||
name="landingPageTitle"
|
||||
defaultValue={org.landingPageTitle ?? ''}
|
||||
placeholder="Zukunft des Handwerks gestalten"
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Hero Untertitel / Text</label>
|
||||
<textarea
|
||||
name="landingPageText"
|
||||
defaultValue={org.landingPageText ?? ''}
|
||||
rows={3}
|
||||
placeholder="Gemeinsam stark für unsere Region."
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Aufmacher Überschrift (Buttons)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="landingPageSectionTitle"
|
||||
defaultValue={org.landingPageSectionTitle ?? ''}
|
||||
placeholder={`${org.name || 'Ihre Innung'} – Gemeinsam stark fürs Handwerk`}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Button Text (CTA)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="landingPageButtonText"
|
||||
defaultValue={org.landingPageButtonText ?? ''}
|
||||
placeholder="Jetzt Mitglied werden"
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="landingPageHeroImage" value={heroImageUrl} />
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Hero Hintergrundbild</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex-1 cursor-pointer">
|
||||
<div className={`px-3 py-2 border rounded-lg text-sm text-center font-medium hover:bg-gray-50 transition-colors ${isUploading.hero ? 'opacity-50' : ''}`}>
|
||||
{isUploading.hero ? 'Wird hochgeladen...' : 'Bild auswählen'}
|
||||
</div>
|
||||
<input type="file" onChange={(e) => handleUpload(e, 'hero')} accept="image/*" className="hidden" disabled={isUploading.hero} />
|
||||
</label>
|
||||
{heroImageUrl && (
|
||||
<button type="button" onClick={() => setHeroImageUrl('')} className="text-red-500 hover:text-red-600 text-sm">
|
||||
Entfernen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1 flex justify-between">
|
||||
<span>Overlay Deckkraft</span>
|
||||
<span className="text-gray-500">{org.landingPageHeroOverlayOpacity ?? 50}%</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
name="landingPageHeroOverlayOpacity"
|
||||
min="0"
|
||||
max="100"
|
||||
defaultValue={org.landingPageHeroOverlayOpacity ?? 50}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Legt fest, wie dunkel der Schleier über dem Hintergrundbild ist, damit der Text gut lesbar bleibt.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Vorteile / Features</label>
|
||||
<textarea
|
||||
name="landingPageFeatures"
|
||||
defaultValue={initialFeatures}
|
||||
rows={5}
|
||||
placeholder="Ein Benefit pro Zeile..."
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">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-sm font-medium text-gray-700 mb-1">App Store URL</label>
|
||||
<input
|
||||
type="url"
|
||||
name="appStoreUrl"
|
||||
defaultValue={org.appStoreUrl ?? ''}
|
||||
placeholder="https://apps.apple.com/..."
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Google Play URL</label>
|
||||
<input
|
||||
type="url"
|
||||
name="playStoreUrl"
|
||||
defaultValue={org.playStoreUrl ?? ''}
|
||||
placeholder="https://play.google.com/..."
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Footer Text</label>
|
||||
<textarea
|
||||
name="landingPageFooter"
|
||||
defaultValue={initialFooter}
|
||||
rows={2}
|
||||
placeholder="© 2024 Innung. Alle Rechte vorbehalten."
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full bg-brand-500 text-white font-medium py-2 px-4 rounded-lg hover:bg-brand-600 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isPending ? 'Wird gespeichert…' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
'use client'
|
||||
|
||||
import { useActionState, useState } from 'react'
|
||||
import { updateOrganization } from '../../actions'
|
||||
|
||||
function jsonToText(value: unknown): string {
|
||||
if (value == null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => (typeof item === 'string' ? item : JSON.stringify(item)))
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
interface Props {
|
||||
org: {
|
||||
id: string
|
||||
name: string
|
||||
plan: string
|
||||
contactEmail: string | null
|
||||
logoUrl: string | null
|
||||
primaryColor: string | null
|
||||
secondaryColor: string | null
|
||||
landingPageTitle: string | null
|
||||
landingPageText: string | null
|
||||
landingPageSectionTitle: string | null
|
||||
landingPageButtonText: string | null
|
||||
landingPageHeroImage: string | null
|
||||
landingPageHeroOverlayOpacity: number | null
|
||||
landingPageFeatures: unknown
|
||||
landingPageFooter: unknown
|
||||
appStoreUrl: string | null
|
||||
playStoreUrl: string | null
|
||||
}
|
||||
}
|
||||
|
||||
const initialState = { success: false, error: '' }
|
||||
|
||||
export function EditOrgForm({ org }: Props) {
|
||||
const boundAction = updateOrganization.bind(null, org.id)
|
||||
const [state, formAction, isPending] = useActionState(boundAction, initialState)
|
||||
const [logoUrl, setLogoUrl] = useState(org.logoUrl || '')
|
||||
const [heroImageUrl, setHeroImageUrl] = useState(org.landingPageHeroImage || '')
|
||||
const [isUploading, setIsUploading] = useState<{ logo?: boolean; hero?: boolean }>({})
|
||||
const [themeColor, setThemeColor] = useState(org.primaryColor || '#E63946')
|
||||
const [secondaryColor, setSecondaryColor] = useState(org.secondaryColor || '#FFFFFF')
|
||||
|
||||
const initialFeatures = jsonToText(org.landingPageFeatures)
|
||||
const initialFooter = jsonToText(org.landingPageFooter)
|
||||
|
||||
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>, type: 'logo' | 'hero') => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
setIsUploading(prev => ({ ...prev, [type]: 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) {
|
||||
if (type === 'logo') setLogoUrl(data.url)
|
||||
if (type === 'hero') setHeroImageUrl(data.url)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload failed', err)
|
||||
} finally {
|
||||
setIsUploading(prev => ({ ...prev, [type]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border p-6">
|
||||
<h2 className="text-base font-semibold text-gray-900 mb-4">Innung bearbeiten</h2>
|
||||
|
||||
{state.success && (
|
||||
<div className="mb-4 p-3 bg-green-50 text-green-700 rounded-lg text-sm">Änderungen gespeichert.</div>
|
||||
)}
|
||||
{state.error && (
|
||||
<div className="mb-4 p-3 bg-red-50 text-red-700 rounded-lg text-sm">{state.error}</div>
|
||||
)}
|
||||
|
||||
<form action={formAction} className="space-y-6">
|
||||
{/* BASISDATEN */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900 border-b pb-2">Basisdaten</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Name der Innung</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
defaultValue={org.name}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Plan</label>
|
||||
<select
|
||||
name="plan"
|
||||
defaultValue={org.plan}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500 bg-white"
|
||||
>
|
||||
<option value="pilot">Pilot</option>
|
||||
<option value="standard">Standard</option>
|
||||
<option value="pro">Pro</option>
|
||||
<option value="verband">Verband</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Kontakt E-Mail</label>
|
||||
<input
|
||||
type="email"
|
||||
name="contactEmail"
|
||||
defaultValue={org.contactEmail ?? ''}
|
||||
placeholder="info@innung.de"
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BRANDING */}
|
||||
<div className="space-y-4 pt-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900 border-b pb-2">Branding</h3>
|
||||
|
||||
<input type="hidden" name="logoUrl" value={logoUrl} />
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Logo</label>
|
||||
<div className="flex items-center gap-3">
|
||||
{logoUrl ? (
|
||||
<div className="w-10 h-10 rounded border bg-gray-50 flex items-center justify-center p-1">
|
||||
<img src={logoUrl} alt="Logo" className="max-w-full max-h-full object-contain" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded border-2 border-dashed 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-5 h-5">
|
||||
<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 cursor-pointer">
|
||||
<div className={`px-3 py-2 border rounded-lg text-sm text-center font-medium hover:bg-gray-50 transition-colors ${isUploading.logo ? 'opacity-50' : ''}`}>
|
||||
{isUploading.logo ? 'Wird hochgeladen...' : 'Logo ändern'}
|
||||
</div>
|
||||
<input type="file" onChange={(e) => handleUpload(e, 'logo')} accept="image/*" className="hidden" disabled={isUploading.logo} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Primärfarbe</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="color"
|
||||
name="primaryColor"
|
||||
value={themeColor}
|
||||
onChange={(e) => setThemeColor(e.target.value)}
|
||||
className="h-9 w-12 p-1 border rounded cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={themeColor}
|
||||
onChange={(e) => setThemeColor(e.target.value)}
|
||||
className="flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 font-mono text-sm"
|
||||
pattern="^#([A-Fa-f0-9]{6})$"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Sekundärfarbe</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="color"
|
||||
name="secondaryColor"
|
||||
value={secondaryColor}
|
||||
onChange={(e) => setSecondaryColor(e.target.value)}
|
||||
className="h-9 w-12 p-1 border rounded cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={secondaryColor}
|
||||
onChange={(e) => setSecondaryColor(e.target.value)}
|
||||
className="flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 font-mono text-sm"
|
||||
pattern="^#([A-Fa-f0-9]{6})$"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* LANDING PAGE */}
|
||||
<div className="space-y-4 pt-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900 border-b pb-2">Landing Page</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Hero Titel</label>
|
||||
<input
|
||||
type="text"
|
||||
name="landingPageTitle"
|
||||
defaultValue={org.landingPageTitle ?? ''}
|
||||
placeholder="Zukunft des Handwerks gestalten"
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Hero Untertitel / Text</label>
|
||||
<textarea
|
||||
name="landingPageText"
|
||||
defaultValue={org.landingPageText ?? ''}
|
||||
rows={3}
|
||||
placeholder="Gemeinsam stark für unsere Region."
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Aufmacher Überschrift (Buttons)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="landingPageSectionTitle"
|
||||
defaultValue={org.landingPageSectionTitle ?? ''}
|
||||
placeholder={`${org.name || 'Ihre Innung'} – Gemeinsam stark fürs Handwerk`}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Button Text (CTA)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="landingPageButtonText"
|
||||
defaultValue={org.landingPageButtonText ?? ''}
|
||||
placeholder="Jetzt Mitglied werden"
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="landingPageHeroImage" value={heroImageUrl} />
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Hero Hintergrundbild</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex-1 cursor-pointer">
|
||||
<div className={`px-3 py-2 border rounded-lg text-sm text-center font-medium hover:bg-gray-50 transition-colors ${isUploading.hero ? 'opacity-50' : ''}`}>
|
||||
{isUploading.hero ? 'Wird hochgeladen...' : 'Bild auswählen'}
|
||||
</div>
|
||||
<input type="file" onChange={(e) => handleUpload(e, 'hero')} accept="image/*" className="hidden" disabled={isUploading.hero} />
|
||||
</label>
|
||||
{heroImageUrl && (
|
||||
<button type="button" onClick={() => setHeroImageUrl('')} className="text-red-500 hover:text-red-600 text-sm">
|
||||
Entfernen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1 flex justify-between">
|
||||
<span>Overlay Deckkraft</span>
|
||||
<span className="text-gray-500">{org.landingPageHeroOverlayOpacity ?? 50}%</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
name="landingPageHeroOverlayOpacity"
|
||||
min="0"
|
||||
max="100"
|
||||
defaultValue={org.landingPageHeroOverlayOpacity ?? 50}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Legt fest, wie dunkel der Schleier über dem Hintergrundbild ist, damit der Text gut lesbar bleibt.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Vorteile / Features</label>
|
||||
<textarea
|
||||
name="landingPageFeatures"
|
||||
defaultValue={initialFeatures}
|
||||
rows={5}
|
||||
placeholder="Ein Benefit pro Zeile..."
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">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-sm font-medium text-gray-700 mb-1">App Store URL</label>
|
||||
<input
|
||||
type="url"
|
||||
name="appStoreUrl"
|
||||
defaultValue={org.appStoreUrl ?? ''}
|
||||
placeholder="https://apps.apple.com/..."
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Google Play URL</label>
|
||||
<input
|
||||
type="url"
|
||||
name="playStoreUrl"
|
||||
defaultValue={org.playStoreUrl ?? ''}
|
||||
placeholder="https://play.google.com/..."
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Footer Text</label>
|
||||
<textarea
|
||||
name="landingPageFooter"
|
||||
defaultValue={initialFooter}
|
||||
rows={2}
|
||||
placeholder="© 2024 Innung. Alle Rechte vorbehalten."
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full bg-brand-500 text-white font-medium py-2 px-4 rounded-lg hover:bg-brand-600 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isPending ? 'Wird gespeichert…' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
'use client'
|
||||
|
||||
'use client'
|
||||
|
||||
import { removeMember } from '../../actions'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function MemberActions({ member, orgId }: { member: { id: string, name: string }, orgId: string }) {
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
|
||||
const handleRemove = async () => {
|
||||
if (!confirm(`Möchten Sie das Mitglied ${member.name} wirklich entfernen?`)) return
|
||||
setIsPending(true)
|
||||
await removeMember(member.id, orgId)
|
||||
setIsPending(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
disabled={isPending}
|
||||
className="text-xs text-red-600 hover:text-red-700 font-medium transition-colors"
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
)
|
||||
}
|
||||
import { useState } from 'react'
|
||||
|
||||
export function MemberActions({ member, orgId }: { member: { id: string, name: string }, orgId: string }) {
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
|
||||
const handleRemove = async () => {
|
||||
if (!confirm(`Möchten Sie das Mitglied ${member.name} wirklich entfernen?`)) return
|
||||
setIsPending(true)
|
||||
await removeMember(member.id, orgId)
|
||||
setIsPending(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
disabled={isPending}
|
||||
className="text-xs text-red-600 hover:text-red-700 font-medium transition-colors"
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
'use client'
|
||||
|
||||
'use client'
|
||||
|
||||
import { removeUserRole, updateUserRole } from '../../actions'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function UserRoleActions({ ur, orgId }: { ur: { id: string, role: string, user: { email: string } }, orgId: string }) {
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
|
||||
const handleRemove = async () => {
|
||||
if (!confirm(`Möchten Sie den Zugriff für ${ur.user.email} wirklich entfernen?`)) return
|
||||
setIsPending(true)
|
||||
await removeUserRole(ur.id, orgId)
|
||||
setIsPending(false)
|
||||
}
|
||||
|
||||
const handleToggleRole = async () => {
|
||||
const newRole = ur.role === 'admin' ? 'member' : 'admin'
|
||||
setIsPending(true)
|
||||
await updateUserRole(ur.id, orgId, newRole)
|
||||
setIsPending(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleToggleRole}
|
||||
disabled={isPending}
|
||||
className="text-xs text-gray-600 hover:text-brand-600 font-medium transition-colors"
|
||||
title={ur.role === 'admin' ? 'Zum Mitglied machen' : 'Zum Admin machen'}
|
||||
>
|
||||
{ur.role === 'admin' ? 'Rolle: Admin' : 'Rolle: Mitglied'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
disabled={isPending}
|
||||
className="text-xs text-red-600 hover:text-red-700 font-medium transition-colors"
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { useState } from 'react'
|
||||
|
||||
export function UserRoleActions({ ur, orgId }: { ur: { id: string, role: string, user: { email: string } }, orgId: string }) {
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
|
||||
const handleRemove = async () => {
|
||||
if (!confirm(`Möchten Sie den Zugriff für ${ur.user.email} wirklich entfernen?`)) return
|
||||
setIsPending(true)
|
||||
await removeUserRole(ur.id, orgId)
|
||||
setIsPending(false)
|
||||
}
|
||||
|
||||
const handleToggleRole = async () => {
|
||||
const newRole = ur.role === 'admin' ? 'member' : 'admin'
|
||||
setIsPending(true)
|
||||
await updateUserRole(ur.id, orgId, newRole)
|
||||
setIsPending(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleToggleRole}
|
||||
disabled={isPending}
|
||||
className="text-xs text-gray-600 hover:text-brand-600 font-medium transition-colors"
|
||||
title={ur.role === 'admin' ? 'Zum Mitglied machen' : 'Zum Admin machen'}
|
||||
>
|
||||
{ur.role === 'admin' ? 'Rolle: Admin' : 'Rolle: Mitglied'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
disabled={isPending}
|
||||
className="text-xs text-red-600 hover:text-red-700 font-medium transition-colors"
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user