340 lines
17 KiB
TypeScript
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>
|
|
)
|
|
}
|