MBO-Tech-IT-Webseite/modules/06-website-cms/files/components/admin/HeroVerwaltung.tsx

340 lines
17 KiB
TypeScript

'use client'
import { useState, useEffect, useRef } from 'react'
interface HeroContent {
site_name: string
site_tagline: string
eyebrow_text: string
headline1: string
headline2: string
subtext1: string
subtext2: string
cta1_text: string; cta1_href: string
cta2_text: string; cta2_href: string
bg_image_path: string | null
}
interface Badge { id: string; text: string; reihenfolge: number }
const inp: React.CSSProperties = {
width: '100%', padding: '8px 12px', borderRadius: '6px',
border: '1px solid var(--border-color)', background: 'var(--bg)',
color: 'var(--text-primary)', fontSize: '14px', boxSizing: 'border-box',
}
const lbl: React.CSSProperties = {
fontSize: '11px', fontWeight: 600, textTransform: 'uppercase',
letterSpacing: '1px', color: 'var(--text-muted)', display: 'block', marginBottom: '6px',
}
const section: React.CSSProperties = {
background: 'var(--surface)', border: '1px solid var(--border-color)',
borderLeft: '3px solid var(--accent)', borderRadius: '8px', padding: '20px',
}
// ← Standardwerte an eigenes Projekt anpassen
const DEFAULTS: HeroContent = {
site_name: 'Musterfirma',
site_tagline: '',
eyebrow_text: 'Ihr Slogan hier',
headline1: 'Wir lieben Qualität.',
headline2: 'Ihre Kunden auch.',
subtext1: '', subtext2: '',
cta1_text: 'Jetzt anfragen', cta1_href: '#kontakt',
cta2_text: 'Unsere Leistungen', cta2_href: '#leistungen',
bg_image_path: null,
}
export function HeroVerwaltung() {
const [content, setContent] = useState<HeroContent>(DEFAULTS)
const [badges, setBadges] = useState<Badge[]>([])
const [newBadge, setNewBadge] = useState('')
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
const [loading, setLoading] = useState(true)
const [uploading, setUploading] = useState(false)
const [bgPreview, setBgPreview] = useState<string | null>(null)
const fileRef = useRef<HTMLInputElement>(null)
const logoRef = useRef<HTMLInputElement>(null)
const faviconRef = useRef<HTMLInputElement>(null)
const [logoPath, setLogoPath] = useState<string | null>(null)
const [faviconPath, setFaviconPath] = useState<string | null>(null)
const [uploadingLogo, setUploadingLogo] = useState(false)
const [uploadingFavicon, setUploadingFavicon] = useState(false)
const supabaseUrl = (process.env.NEXT_PUBLIC_SUPABASE_URL ?? '').replace(/\/$/, '')
useEffect(() => {
fetch('/api/admin/hero').then(r => r.json()).then(({ content: c, badges: b }) => {
if (c) {
setContent({ ...DEFAULTS, ...c })
if (c.logo_path) setLogoPath(c.logo_path)
if (c.favicon_path) setFaviconPath(c.favicon_path)
}
setBadges(b ?? [])
setLoading(false)
})
}, [])
const bgUrl = content.bg_image_path
? `${supabaseUrl}/storage/v1/object/public/hero-bilder/${content.bg_image_path}`
: null
async function handleSave(e: React.FormEvent) {
e.preventDefault()
setSaving(true)
await fetch('/api/admin/hero', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(content),
})
setSaving(false); setSaved(true); setTimeout(() => setSaved(false), 2000)
}
async function uploadBg(file: File) {
setUploading(true)
setBgPreview(URL.createObjectURL(file))
const fd = new FormData()
fd.append('file', file)
const res = await fetch('/api/admin/hero/bild', { method: 'POST', body: fd })
const json = await res.json()
if (res.ok) setContent(p => ({ ...p, bg_image_path: json.path }))
setUploading(false)
if (fileRef.current) fileRef.current.value = ''
}
async function deleteBg() {
await fetch('/api/admin/hero/bild', { method: 'DELETE' })
setContent(p => ({ ...p, bg_image_path: null }))
setBgPreview(null)
}
async function uploadLogo(file: File) {
setUploadingLogo(true)
const fd = new FormData(); fd.append('file', file)
const res = await fetch('/api/admin/hero/logo', { method: 'POST', body: fd })
const json = await res.json()
if (res.ok) setLogoPath(json.path)
setUploadingLogo(false)
if (logoRef.current) logoRef.current.value = ''
}
async function deleteLogo() {
await fetch('/api/admin/hero/logo', { method: 'DELETE' })
setLogoPath(null)
}
async function uploadFavicon(file: File) {
setUploadingFavicon(true)
const fd = new FormData(); fd.append('file', file)
const res = await fetch('/api/admin/hero/favicon', { method: 'POST', body: fd })
const json = await res.json()
if (res.ok) setFaviconPath(json.path)
setUploadingFavicon(false)
if (faviconRef.current) faviconRef.current.value = ''
}
async function deleteFavicon() {
await fetch('/api/admin/hero/favicon', { method: 'DELETE' })
setFaviconPath(null)
}
async function addBadge() {
if (!newBadge.trim()) return
const res = await fetch('/api/admin/hero/badges', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: newBadge.trim(), reihenfolge: badges.length }),
})
const { badge } = await res.json()
setBadges(prev => [...prev, badge]); setNewBadge('')
}
async function deleteBadge(id: string) {
await fetch(`/api/admin/hero/badges/${id}`, { method: 'DELETE' })
setBadges(prev => prev.filter(b => b.id !== id))
}
if (loading) return <p style={{ color: 'var(--text-muted)' }}>Lade</p>
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<form onSubmit={handleSave} style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px', alignItems: 'start' }}>
{/* Linke Spalte: Texte */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={section}>
<p style={{ fontSize: '13px', fontWeight: 700, color: 'var(--text-primary)', marginBottom: '16px' }}>Navbar (Logo)</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<label style={lbl}>Seitenname (Titel)</label>
<input style={inp} value={content.site_name} onChange={e => setContent(p => ({ ...p, site_name: e.target.value }))} placeholder="z. B. Musterfirma" />
</div>
<div>
<label style={lbl}>Tagline (Untertitel)</label>
<input style={inp} value={content.site_tagline} onChange={e => setContent(p => ({ ...p, site_tagline: e.target.value }))} placeholder="z. B. Ihr Dienstleister aus der Region" />
</div>
</div>
</div>
<div style={section}>
<p style={{ fontSize: '13px', fontWeight: 700, color: 'var(--text-primary)', marginBottom: '16px' }}>Überschriften</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<label style={lbl}>Eyebrow-Text</label>
<input style={inp} value={content.eyebrow_text} onChange={e => setContent(p => ({ ...p, eyebrow_text: e.target.value }))} placeholder="z. B. Ihr Slogan hier" />
</div>
<div>
<label style={lbl}>Überschrift Zeile 1 (weiß)</label>
<input style={inp} value={content.headline1} onChange={e => setContent(p => ({ ...p, headline1: e.target.value }))} placeholder="z. B. Wir lieben Qualität." />
</div>
<div>
<label style={lbl}>Überschrift Zeile 2 (Akzentfarbe)</label>
<input style={inp} value={content.headline2} onChange={e => setContent(p => ({ ...p, headline2: e.target.value }))} placeholder="z. B. Ihre Kunden auch." />
</div>
</div>
</div>
<div style={section}>
<p style={{ fontSize: '13px', fontWeight: 700, color: 'var(--text-primary)', marginBottom: '16px' }}>Fließtexte</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<label style={lbl}>Subtext 1 (groß)</label>
<textarea rows={3} style={{ ...inp, resize: 'vertical' }} value={content.subtext1} onChange={e => setContent(p => ({ ...p, subtext1: e.target.value }))} />
</div>
<div>
<label style={lbl}>Subtext 2 (klein)</label>
<textarea rows={3} style={{ ...inp, resize: 'vertical' }} value={content.subtext2} onChange={e => setContent(p => ({ ...p, subtext2: e.target.value }))} />
</div>
</div>
</div>
<div style={section}>
<p style={{ fontSize: '13px', fontWeight: 700, color: 'var(--text-primary)', marginBottom: '16px' }}>Buttons</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<label style={lbl}>Button 1 (primär)</label>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' }}>
<input style={inp} value={content.cta1_text} onChange={e => setContent(p => ({ ...p, cta1_text: e.target.value }))} placeholder="Jetzt anfragen" />
<input style={inp} value={content.cta1_href} onChange={e => setContent(p => ({ ...p, cta1_href: e.target.value }))} placeholder="#kontakt" />
</div>
</div>
<div>
<label style={lbl}>Button 2 (sekundär)</label>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' }}>
<input style={inp} value={content.cta2_text} onChange={e => setContent(p => ({ ...p, cta2_text: e.target.value }))} placeholder="Unsere Leistungen" />
<input style={inp} value={content.cta2_href} onChange={e => setContent(p => ({ ...p, cta2_href: e.target.value }))} placeholder="#leistungen" />
</div>
</div>
</div>
</div>
<button type="submit" disabled={saving} style={{ padding: '10px', borderRadius: '6px', background: 'var(--accent)', color: '#fff', fontWeight: 700, border: 'none', cursor: 'pointer', fontSize: '14px' }}>
{saving ? 'Speichert…' : saved ? '✓ Gespeichert' : 'Alles speichern'}
</button>
</div>
{/* Rechte Spalte: Bilder + Badges */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{/* Logo */}
<div style={section}>
<p style={{ fontSize: '13px', fontWeight: 700, color: 'var(--text-primary)', marginBottom: '16px' }}>Logo</p>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px', padding: '12px', borderRadius: '6px', background: 'var(--bg)', border: '1px solid var(--border-color)', minHeight: '56px' }}>
{logoPath
? /* eslint-disable-next-line @next/next/no-img-element */
<img src={`${supabaseUrl}/storage/v1/object/public/site-assets/${logoPath}`} alt="Logo" style={{ height: '40px', width: 'auto', objectFit: 'contain' }} />
: <span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>Kein Logo Textname wird angezeigt</span>
}
{logoPath && (
<button type="button" onClick={deleteLogo} style={{ marginLeft: 'auto', padding: '4px 10px', borderRadius: '4px', background: 'transparent', color: '#f87171', border: '1px solid rgba(220,38,38,0.3)', cursor: 'pointer', fontSize: '12px' }}> Entfernen</button>
)}
</div>
<input ref={logoRef} type="file" accept="image/*" style={{ display: 'none' }} onChange={e => { const f = e.target.files?.[0]; if (f) uploadLogo(f) }} />
<button type="button" onClick={() => logoRef.current?.click()} disabled={uploadingLogo} style={{ width: '100%', padding: '8px', borderRadius: '6px', background: 'transparent', border: '1px dashed var(--border-color)', color: 'var(--text-muted)', cursor: 'pointer', fontSize: '13px' }}>
{uploadingLogo ? 'Wird hochgeladen…' : '+ Logo hochladen (PNG/SVG empfohlen)'}
</button>
</div>
{/* Favicon */}
<div style={section}>
<p style={{ fontSize: '13px', fontWeight: 700, color: 'var(--text-primary)', marginBottom: '16px' }}>Favicon (Browser-Tab-Icon)</p>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px', padding: '12px', borderRadius: '6px', background: 'var(--bg)', border: '1px solid var(--border-color)', minHeight: '56px' }}>
{faviconPath
? /* eslint-disable-next-line @next/next/no-img-element */
<img src={`${supabaseUrl}/storage/v1/object/public/site-assets/${faviconPath}`} alt="Favicon" style={{ width: '32px', height: '32px', objectFit: 'contain' }} />
: <span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>Kein Favicon Standard-Icon wird verwendet</span>
}
{faviconPath && (
<button type="button" onClick={deleteFavicon} style={{ marginLeft: 'auto', padding: '4px 10px', borderRadius: '4px', background: 'transparent', color: '#f87171', border: '1px solid rgba(220,38,38,0.3)', cursor: 'pointer', fontSize: '12px' }}> Entfernen</button>
)}
</div>
<input ref={faviconRef} type="file" accept="image/*, .ico" style={{ display: 'none' }} onChange={e => { const f = e.target.files?.[0]; if (f) uploadFavicon(f) }} />
<button type="button" onClick={() => faviconRef.current?.click()} disabled={uploadingFavicon} style={{ width: '100%', padding: '8px', borderRadius: '6px', background: 'transparent', border: '1px dashed var(--border-color)', color: 'var(--text-muted)', cursor: 'pointer', fontSize: '13px' }}>
{uploadingFavicon ? 'Wird hochgeladen…' : '+ Favicon hochladen (.ico / PNG / SVG)'}
</button>
</div>
{/* Hintergrundbild */}
<div style={section}>
<p style={{ fontSize: '13px', fontWeight: 700, color: 'var(--text-primary)', marginBottom: '16px' }}>Hintergrundbild</p>
<div style={{ position: 'relative', width: '100%', aspectRatio: '16/7', borderRadius: '6px', overflow: 'hidden', marginBottom: '12px', background: 'var(--bg)', border: '1px solid var(--border-color)' }}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={bgPreview ?? bgUrl ?? '/hero.jpg'}
alt="Hero Hintergrund"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
{content.bg_image_path && (
<button
type="button"
onClick={deleteBg}
style={{ position: 'absolute', top: '8px', right: '8px', padding: '4px 10px', borderRadius: '4px', background: 'rgba(0,0,0,0.6)', color: '#f87171', border: 'none', cursor: 'pointer', fontSize: '12px', fontWeight: 600 }}
>
Zurücksetzen
</button>
)}
</div>
<input ref={fileRef} type="file" accept="image/*" style={{ display: 'none' }} onChange={e => { const f = e.target.files?.[0]; if (f) uploadBg(f) }} />
<button
type="button"
onClick={() => fileRef.current?.click()}
disabled={uploading}
style={{ width: '100%', padding: '8px', borderRadius: '6px', background: 'transparent', border: '1px dashed var(--border-color)', color: 'var(--text-muted)', cursor: 'pointer', fontSize: '13px' }}
>
{uploading ? 'Wird hochgeladen…' : '+ Bild hochladen oder ersetzen'}
</button>
{!content.bg_image_path && (
<p style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '6px' }}>
Kein Bild gesetzt Standard: /hero.jpg
</p>
)}
</div>
{/* Trust-Badges */}
<div style={section}>
<p style={{ fontSize: '13px', fontWeight: 700, color: 'var(--text-primary)', marginBottom: '16px' }}>Trust-Badges</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '12px' }}>
{badges.map(b => (
<div key={b.id} style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<span style={{ flex: 1, padding: '8px 12px', borderRadius: '6px', background: 'var(--bg)', border: '1px solid var(--border-color)', fontSize: '13px', color: 'var(--text-primary)' }}>
{b.text}
</span>
<button type="button" onClick={() => deleteBadge(b.id)} style={{ padding: '6px 10px', borderRadius: '6px', background: 'transparent', border: '1px solid rgba(220,38,38,0.3)', color: '#f87171', cursor: 'pointer', fontSize: '12px' }}></button>
</div>
))}
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<input
style={{ ...inp, flex: 1 }}
value={newBadge}
onChange={e => setNewBadge(e.target.value)}
placeholder="z. B. Privatkunden"
onKeyDown={e => e.key === 'Enter' && (e.preventDefault(), addBadge())}
/>
<button type="button" onClick={addBadge} style={{ padding: '8px 14px', borderRadius: '6px', background: 'var(--accent)', color: '#fff', fontWeight: 700, border: 'none', cursor: 'pointer', fontSize: '13px' }}>+</button>
</div>
</div>
</div>
</form>
</div>
)
}