feat(website): Add Foerderantrag pages and website management section
Add public Foerderantrag (funding application) pages to the website. Add website management section to admin-v2 with content and status APIs. Extend Header, LandingContent, and i18n with new navigation entries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
670
admin-v2/app/(admin)/website/manager/page.tsx
Normal file
670
admin-v2/app/(admin)/website/manager/page.tsx
Normal file
@@ -0,0 +1,670 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Website Manager - CMS Dashboard
|
||||||
|
*
|
||||||
|
* Visual CMS dashboard for the BreakPilot website (macmini:3000).
|
||||||
|
* 60/40 split: Section cards with inline editors | Live iframe preview.
|
||||||
|
* Status bar, content stats, reset, save.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
|
import type {
|
||||||
|
WebsiteContent,
|
||||||
|
HeroContent,
|
||||||
|
FeatureContent,
|
||||||
|
FAQItem,
|
||||||
|
PricingPlan,
|
||||||
|
} from '@/lib/content-types'
|
||||||
|
|
||||||
|
const ADMIN_KEY = 'breakpilot-admin-2024'
|
||||||
|
|
||||||
|
// Section metadata for cards
|
||||||
|
const SECTIONS = [
|
||||||
|
{ key: 'hero', name: 'Hero Section', icon: '🎯', scrollTo: 'hero' },
|
||||||
|
{ key: 'features', name: 'Features', icon: '⚡', scrollTo: 'features' },
|
||||||
|
{ key: 'faq', name: 'FAQ', icon: '❓', scrollTo: 'faq' },
|
||||||
|
{ key: 'pricing', name: 'Pricing', icon: '💰', scrollTo: 'pricing' },
|
||||||
|
{ key: 'trust', name: 'Trust Indicators', icon: '🛡️', scrollTo: 'trust' },
|
||||||
|
{ key: 'testimonial', name: 'Testimonial', icon: '💬', scrollTo: 'trust' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
type SectionKey = (typeof SECTIONS)[number]['key']
|
||||||
|
|
||||||
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function countWords(content: WebsiteContent): number {
|
||||||
|
const texts: string[] = []
|
||||||
|
// Hero
|
||||||
|
texts.push(content.hero.badge, content.hero.title, content.hero.titleHighlight1, content.hero.titleHighlight2, content.hero.subtitle, content.hero.ctaPrimary, content.hero.ctaSecondary, content.hero.ctaHint)
|
||||||
|
// Features
|
||||||
|
content.features.forEach(f => { texts.push(f.title, f.description) })
|
||||||
|
// FAQ
|
||||||
|
content.faq.forEach(f => { texts.push(f.question, ...f.answer) })
|
||||||
|
// Pricing
|
||||||
|
content.pricing.forEach(p => { texts.push(p.name, p.description, p.features.tasks, p.features.taskDescription, ...p.features.included) })
|
||||||
|
// Trust
|
||||||
|
texts.push(content.trust.item1.value, content.trust.item1.label, content.trust.item2.value, content.trust.item2.label, content.trust.item3.value, content.trust.item3.label)
|
||||||
|
// Testimonial
|
||||||
|
texts.push(content.testimonial.quote, content.testimonial.author, content.testimonial.role)
|
||||||
|
return texts.filter(Boolean).join(' ').split(/\s+/).filter(Boolean).length
|
||||||
|
}
|
||||||
|
|
||||||
|
function sectionComplete(content: WebsiteContent, section: SectionKey): boolean {
|
||||||
|
switch (section) {
|
||||||
|
case 'hero':
|
||||||
|
return !!(content.hero.title && content.hero.subtitle && content.hero.ctaPrimary)
|
||||||
|
case 'features':
|
||||||
|
return content.features.length > 0 && content.features.every(f => f.title && f.description)
|
||||||
|
case 'faq':
|
||||||
|
return content.faq.length > 0 && content.faq.every(f => f.question && f.answer.length > 0)
|
||||||
|
case 'pricing':
|
||||||
|
return content.pricing.length > 0 && content.pricing.every(p => p.name && p.price > 0)
|
||||||
|
case 'trust':
|
||||||
|
return !!(content.trust.item1.value && content.trust.item2.value && content.trust.item3.value)
|
||||||
|
case 'testimonial':
|
||||||
|
return !!(content.testimonial.quote && content.testimonial.author)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sectionSummary(content: WebsiteContent, section: SectionKey): string {
|
||||||
|
switch (section) {
|
||||||
|
case 'hero':
|
||||||
|
return `"${content.hero.title} ${content.hero.titleHighlight1}"`.slice(0, 50)
|
||||||
|
case 'features':
|
||||||
|
return `${content.features.length} Features`
|
||||||
|
case 'faq':
|
||||||
|
return `${content.faq.length} Fragen`
|
||||||
|
case 'pricing':
|
||||||
|
return `${content.pricing.length} Plaene`
|
||||||
|
case 'trust':
|
||||||
|
return `${content.trust.item1.value}, ${content.trust.item2.value}, ${content.trust.item3.value}`
|
||||||
|
case 'testimonial':
|
||||||
|
return `"${content.testimonial.quote.slice(0, 40)}..."`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main Component ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export default function WebsiteManagerPage() {
|
||||||
|
const [content, setContent] = useState<WebsiteContent | null>(null)
|
||||||
|
const [originalContent, setOriginalContent] = useState<WebsiteContent | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
||||||
|
const [expandedSection, setExpandedSection] = useState<SectionKey | null>(null)
|
||||||
|
const [websiteStatus, setWebsiteStatus] = useState<{ online: boolean; responseTime: number } | null>(null)
|
||||||
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||||
|
|
||||||
|
// Load content
|
||||||
|
useEffect(() => {
|
||||||
|
loadContent()
|
||||||
|
checkWebsiteStatus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Auto-dismiss messages
|
||||||
|
useEffect(() => {
|
||||||
|
if (message) {
|
||||||
|
const t = setTimeout(() => setMessage(null), 4000)
|
||||||
|
return () => clearTimeout(t)
|
||||||
|
}
|
||||||
|
}, [message])
|
||||||
|
|
||||||
|
async function loadContent() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/website/content')
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
setContent(data)
|
||||||
|
setOriginalContent(JSON.parse(JSON.stringify(data)))
|
||||||
|
} else {
|
||||||
|
setMessage({ type: 'error', text: 'Fehler beim Laden des Contents' })
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setMessage({ type: 'error', text: 'Verbindungsfehler beim Laden' })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkWebsiteStatus() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/website/status')
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
setWebsiteStatus(data)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setWebsiteStatus({ online: false, responseTime: 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveChanges() {
|
||||||
|
if (!content) return
|
||||||
|
setSaving(true)
|
||||||
|
setMessage(null)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/website/content', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'x-admin-key': ADMIN_KEY },
|
||||||
|
body: JSON.stringify(content),
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
setMessage({ type: 'success', text: 'Erfolgreich gespeichert!' })
|
||||||
|
setOriginalContent(JSON.parse(JSON.stringify(content)))
|
||||||
|
// Reload iframe to reflect changes
|
||||||
|
if (iframeRef.current) {
|
||||||
|
iframeRef.current.src = iframeRef.current.src
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const err = await res.json()
|
||||||
|
setMessage({ type: 'error', text: err.error || 'Fehler beim Speichern' })
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setMessage({ type: 'error', text: 'Verbindungsfehler beim Speichern' })
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetContent() {
|
||||||
|
if (originalContent) {
|
||||||
|
setContent(JSON.parse(JSON.stringify(originalContent)))
|
||||||
|
setMessage({ type: 'success', text: 'Zurueckgesetzt auf letzten gespeicherten Stand' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll iframe to section
|
||||||
|
const scrollPreview = useCallback((scrollTo: string) => {
|
||||||
|
if (!iframeRef.current?.contentWindow) return
|
||||||
|
try {
|
||||||
|
iframeRef.current.contentWindow.postMessage(
|
||||||
|
{ type: 'scrollTo', section: scrollTo },
|
||||||
|
'*'
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
// cross-origin fallback
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function toggleSection(key: SectionKey) {
|
||||||
|
const newExpanded = expandedSection === key ? null : key
|
||||||
|
setExpandedSection(newExpanded)
|
||||||
|
if (newExpanded) {
|
||||||
|
const section = SECTIONS.find(s => s.key === newExpanded)
|
||||||
|
if (section) scrollPreview(section.scrollTo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Render ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-20">
|
||||||
|
<div className="flex items-center gap-3 text-slate-500">
|
||||||
|
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
Lade Website-Content...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-20">
|
||||||
|
<div className="text-red-600">Content konnte nicht geladen werden.</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const wordCount = countWords(content)
|
||||||
|
const completeSections = SECTIONS.filter(s => sectionComplete(content, s.key)).length
|
||||||
|
const completionPct = Math.round((completeSections / SECTIONS.length) * 100)
|
||||||
|
const hasChanges = JSON.stringify(content) !== JSON.stringify(originalContent)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* ── Status Bar ───────────────────────────────────────────────────── */}
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 px-5 py-3 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Website status */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`w-2.5 h-2.5 rounded-full ${websiteStatus?.online ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||||
|
<span className="text-sm text-slate-700">
|
||||||
|
Website {websiteStatus?.online ? 'online' : 'offline'}
|
||||||
|
{websiteStatus?.online && websiteStatus.responseTime > 0 && (
|
||||||
|
<span className="text-slate-400 ml-1">({websiteStatus.responseTime}ms)</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Link */}
|
||||||
|
<a
|
||||||
|
href="https://macmini:3000"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-sm text-sky-600 hover:text-sky-700 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Zur Website
|
||||||
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{message && (
|
||||||
|
<span className={`px-3 py-1 rounded-lg text-sm font-medium ${
|
||||||
|
message.type === 'success' ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
|
||||||
|
}`}>
|
||||||
|
{message.text}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={resetContent}
|
||||||
|
disabled={!hasChanges}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-slate-600 bg-slate-100 rounded-lg hover:bg-slate-200 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={saveChanges}
|
||||||
|
disabled={saving || !hasChanges}
|
||||||
|
className="px-5 py-2 text-sm font-medium text-white bg-sky-600 rounded-lg hover:bg-sky-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{saving ? 'Speichern...' : 'Speichern'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Stats Bar ────────────────────────────────────────────────────── */}
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
{[
|
||||||
|
{ label: 'Sektionen', value: `${SECTIONS.length}`, icon: '📄' },
|
||||||
|
{ label: 'Woerter', value: wordCount.toLocaleString('de-DE'), icon: '📝' },
|
||||||
|
{ label: 'Vollstaendig', value: `${completionPct}%`, icon: completionPct === 100 ? '✅' : '🔧' },
|
||||||
|
{ label: 'Aenderungen', value: hasChanges ? 'Ungespeichert' : 'Aktuell', icon: hasChanges ? '🟡' : '🟢' },
|
||||||
|
].map((stat) => (
|
||||||
|
<div key={stat.label} className="bg-white rounded-xl border border-slate-200 px-4 py-3 flex items-center gap-3">
|
||||||
|
<span className="text-xl">{stat.icon}</span>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-semibold text-slate-900">{stat.value}</div>
|
||||||
|
<div className="text-xs text-slate-500">{stat.label}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Main Layout: 60/40 ───────────────────────────────────────────── */}
|
||||||
|
<div className="grid grid-cols-5 gap-4" style={{ height: 'calc(100vh - 300px)' }}>
|
||||||
|
{/* ── Left: Section Cards (3/5 = 60%) ──────────────────────────── */}
|
||||||
|
<div className="col-span-3 overflow-y-auto pr-1 space-y-3">
|
||||||
|
{SECTIONS.map((section) => {
|
||||||
|
const isExpanded = expandedSection === section.key
|
||||||
|
const isComplete = sectionComplete(content, section.key)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={section.key}
|
||||||
|
className={`bg-white rounded-xl border transition-all ${
|
||||||
|
isExpanded ? 'border-sky-300 shadow-md' : 'border-slate-200 hover:border-slate-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Card Header */}
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSection(section.key)}
|
||||||
|
className="w-full px-5 py-4 flex items-center justify-between text-left"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-xl">{section.icon}</span>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-slate-900">{section.name}</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-0.5">{sectionSummary(content, section.key)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{isComplete ? (
|
||||||
|
<span className="w-6 h-6 rounded-full bg-green-100 text-green-600 flex items-center justify-center text-xs">
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="w-6 h-6 rounded-full bg-amber-100 text-amber-600 flex items-center justify-center text-xs">!</span>
|
||||||
|
)}
|
||||||
|
<svg
|
||||||
|
className={`w-5 h-5 text-slate-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
||||||
|
fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Inline Editor */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="px-5 pb-5 border-t border-slate-100 pt-4">
|
||||||
|
{section.key === 'hero' && <HeroEditor content={content} setContent={setContent} />}
|
||||||
|
{section.key === 'features' && <FeaturesEditor content={content} setContent={setContent} />}
|
||||||
|
{section.key === 'faq' && <FAQEditor content={content} setContent={setContent} />}
|
||||||
|
{section.key === 'pricing' && <PricingEditor content={content} setContent={setContent} />}
|
||||||
|
{section.key === 'trust' && <TrustEditor content={content} setContent={setContent} />}
|
||||||
|
{section.key === 'testimonial' && <TestimonialEditor content={content} setContent={setContent} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Right: Live Preview (2/5 = 40%) ──────────────────────────── */}
|
||||||
|
<div className="col-span-2 bg-white rounded-xl border border-slate-200 overflow-hidden flex flex-col">
|
||||||
|
{/* Preview Header */}
|
||||||
|
<div className="bg-slate-50 border-b border-slate-200 px-4 py-2.5 flex items-center justify-between flex-shrink-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<div className="w-2.5 h-2.5 rounded-full bg-red-400" />
|
||||||
|
<div className="w-2.5 h-2.5 rounded-full bg-yellow-400" />
|
||||||
|
<div className="w-2.5 h-2.5 rounded-full bg-green-400" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-slate-500 ml-2">macmini:3000</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => { if (iframeRef.current) iframeRef.current.src = iframeRef.current.src }}
|
||||||
|
className="p-1 text-slate-400 hover:text-slate-600 rounded transition-colors"
|
||||||
|
title="Preview neu laden"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* iframe */}
|
||||||
|
<div className="flex-1 relative bg-slate-100">
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
src="https://macmini:3000/?preview=true"
|
||||||
|
className="absolute inset-0 w-full h-full border-0"
|
||||||
|
style={{
|
||||||
|
width: '166.67%',
|
||||||
|
height: '166.67%',
|
||||||
|
transform: 'scale(0.6)',
|
||||||
|
transformOrigin: 'top left',
|
||||||
|
}}
|
||||||
|
title="Website Preview"
|
||||||
|
sandbox="allow-same-origin allow-scripts"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Section Editors ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface EditorProps {
|
||||||
|
content: WebsiteContent
|
||||||
|
setContent: React.Dispatch<React.SetStateAction<WebsiteContent | null>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputCls = 'w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition-colors'
|
||||||
|
const labelCls = 'block text-xs font-medium text-slate-600 mb-1'
|
||||||
|
|
||||||
|
// ─── Hero Editor ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function HeroEditor({ content, setContent }: EditorProps) {
|
||||||
|
function update(field: keyof HeroContent, value: string) {
|
||||||
|
setContent(c => c ? { ...c, hero: { ...c.hero, [field]: value } } : c)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Badge</label>
|
||||||
|
<input className={inputCls} value={content.hero.badge} onChange={e => update('badge', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Titel</label>
|
||||||
|
<input className={inputCls} value={content.hero.title} onChange={e => update('title', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Highlight 1</label>
|
||||||
|
<input className={inputCls} value={content.hero.titleHighlight1} onChange={e => update('titleHighlight1', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Highlight 2</label>
|
||||||
|
<input className={inputCls} value={content.hero.titleHighlight2} onChange={e => update('titleHighlight2', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Untertitel</label>
|
||||||
|
<textarea className={inputCls} rows={2} value={content.hero.subtitle} onChange={e => update('subtitle', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>CTA Primaer</label>
|
||||||
|
<input className={inputCls} value={content.hero.ctaPrimary} onChange={e => update('ctaPrimary', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>CTA Sekundaer</label>
|
||||||
|
<input className={inputCls} value={content.hero.ctaSecondary} onChange={e => update('ctaSecondary', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>CTA Hinweis</label>
|
||||||
|
<input className={inputCls} value={content.hero.ctaHint} onChange={e => update('ctaHint', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Features Editor ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function FeaturesEditor({ content, setContent }: EditorProps) {
|
||||||
|
function update(index: number, field: keyof FeatureContent, value: string) {
|
||||||
|
setContent(c => {
|
||||||
|
if (!c) return c
|
||||||
|
const features = [...c.features]
|
||||||
|
features[index] = { ...features[index], [field]: value }
|
||||||
|
return { ...c, features }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{content.features.map((feature, i) => (
|
||||||
|
<div key={feature.id} className="bg-slate-50 rounded-lg p-3 space-y-2">
|
||||||
|
<div className="grid grid-cols-6 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Icon</label>
|
||||||
|
<input className={`${inputCls} text-center text-lg`} value={feature.icon} onChange={e => update(i, 'icon', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-5">
|
||||||
|
<label className={labelCls}>Titel</label>
|
||||||
|
<input className={inputCls} value={feature.title} onChange={e => update(i, 'title', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Beschreibung</label>
|
||||||
|
<textarea className={inputCls} rows={2} value={feature.description} onChange={e => update(i, 'description', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── FAQ Editor ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function FAQEditor({ content, setContent }: EditorProps) {
|
||||||
|
function updateItem(index: number, field: 'question' | 'answer', value: string) {
|
||||||
|
setContent(c => {
|
||||||
|
if (!c) return c
|
||||||
|
const faq = [...c.faq]
|
||||||
|
if (field === 'answer') {
|
||||||
|
faq[index] = { ...faq[index], answer: value.split('\n') }
|
||||||
|
} else {
|
||||||
|
faq[index] = { ...faq[index], question: value }
|
||||||
|
}
|
||||||
|
return { ...c, faq }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function addItem() {
|
||||||
|
setContent(c => c ? { ...c, faq: [...c.faq, { question: 'Neue Frage?', answer: ['Antwort hier...'] }] } : c)
|
||||||
|
}
|
||||||
|
function removeItem(index: number) {
|
||||||
|
setContent(c => c ? { ...c, faq: c.faq.filter((_, i) => i !== index) } : c)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{content.faq.map((item, i) => (
|
||||||
|
<div key={i} className="bg-slate-50 rounded-lg p-3 space-y-2 relative group">
|
||||||
|
<button
|
||||||
|
onClick={() => removeItem(i)}
|
||||||
|
className="absolute top-2 right-2 p-1 text-red-400 hover:text-red-600 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
title="Entfernen"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Frage {i + 1}</label>
|
||||||
|
<input className={inputCls} value={item.question} onChange={e => updateItem(i, 'question', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Antwort</label>
|
||||||
|
<textarea className={`${inputCls} font-mono`} rows={3} value={item.answer.join('\n')} onChange={e => updateItem(i, 'answer', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button onClick={addItem} className="w-full py-2 border-2 border-dashed border-slate-300 rounded-lg text-sm text-slate-500 hover:border-sky-400 hover:text-sky-600 transition-colors">
|
||||||
|
+ Frage hinzufuegen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Pricing Editor ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function PricingEditor({ content, setContent }: EditorProps) {
|
||||||
|
function update(index: number, field: string, value: string | number | boolean) {
|
||||||
|
setContent(c => {
|
||||||
|
if (!c) return c
|
||||||
|
const pricing = [...c.pricing]
|
||||||
|
if (field === 'price') {
|
||||||
|
pricing[index] = { ...pricing[index], price: Number(value) }
|
||||||
|
} else if (field === 'popular') {
|
||||||
|
pricing[index] = { ...pricing[index], popular: Boolean(value) }
|
||||||
|
} else if (field.startsWith('features.')) {
|
||||||
|
const sub = field.replace('features.', '')
|
||||||
|
if (sub === 'included' && typeof value === 'string') {
|
||||||
|
pricing[index] = { ...pricing[index], features: { ...pricing[index].features, included: value.split('\n') } }
|
||||||
|
} else {
|
||||||
|
pricing[index] = { ...pricing[index], features: { ...pricing[index].features, [sub]: value } }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pricing[index] = { ...pricing[index], [field]: value }
|
||||||
|
}
|
||||||
|
return { ...c, pricing }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{content.pricing.map((plan, i) => (
|
||||||
|
<div key={plan.id} className="bg-slate-50 rounded-lg p-3 space-y-2">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="text-sm font-semibold text-slate-800">{plan.name}</span>
|
||||||
|
{plan.popular && <span className="text-xs bg-sky-100 text-sky-700 px-1.5 py-0.5 rounded">Beliebt</span>}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Name</label>
|
||||||
|
<input className={inputCls} value={plan.name} onChange={e => update(i, 'name', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Preis (EUR)</label>
|
||||||
|
<input className={inputCls} type="number" step="0.01" value={plan.price} onChange={e => update(i, 'price', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Intervall</label>
|
||||||
|
<input className={inputCls} value={plan.interval} onChange={e => update(i, 'interval', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end pb-1">
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" checked={plan.popular || false} onChange={e => update(i, 'popular', e.target.checked)} className="w-4 h-4 text-sky-600 rounded" />
|
||||||
|
<span className="text-xs text-slate-600">Beliebt</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Beschreibung</label>
|
||||||
|
<input className={inputCls} value={plan.description} onChange={e => update(i, 'description', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Aufgaben</label>
|
||||||
|
<input className={inputCls} value={plan.features.tasks} onChange={e => update(i, 'features.tasks', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Aufgaben-Beschreibung</label>
|
||||||
|
<input className={inputCls} value={plan.features.taskDescription} onChange={e => update(i, 'features.taskDescription', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Features (eine pro Zeile)</label>
|
||||||
|
<textarea className={`${inputCls} font-mono`} rows={3} value={plan.features.included.join('\n')} onChange={e => update(i, 'features.included', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Trust Editor ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function TrustEditor({ content, setContent }: EditorProps) {
|
||||||
|
function update(key: 'item1' | 'item2' | 'item3', field: 'value' | 'label', val: string) {
|
||||||
|
setContent(c => c ? { ...c, trust: { ...c.trust, [key]: { ...c.trust[key], [field]: val } } } : c)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
{(['item1', 'item2', 'item3'] as const).map((key, i) => (
|
||||||
|
<div key={key} className="bg-slate-50 rounded-lg p-3 space-y-2">
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Wert {i + 1}</label>
|
||||||
|
<input className={inputCls} value={content.trust[key].value} onChange={e => update(key, 'value', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Label {i + 1}</label>
|
||||||
|
<input className={inputCls} value={content.trust[key].label} onChange={e => update(key, 'label', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Testimonial Editor ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function TestimonialEditor({ content, setContent }: EditorProps) {
|
||||||
|
function update(field: 'quote' | 'author' | 'role', value: string) {
|
||||||
|
setContent(c => c ? { ...c, testimonial: { ...c.testimonial, [field]: value } } : c)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Zitat</label>
|
||||||
|
<textarea className={inputCls} rows={3} value={content.testimonial.quote} onChange={e => update('quote', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Autor</label>
|
||||||
|
<input className={inputCls} value={content.testimonial.author} onChange={e => update('author', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>Rolle</label>
|
||||||
|
<input className={inputCls} value={content.testimonial.role} onChange={e => update('role', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
51
admin-v2/app/(admin)/website/page.tsx
Normal file
51
admin-v2/app/(admin)/website/page.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { getCategoryById } from '@/lib/navigation'
|
||||||
|
import { ModuleCard } from '@/components/common/ModuleCard'
|
||||||
|
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||||
|
|
||||||
|
export default function WebsitePage() {
|
||||||
|
const category = getCategoryById('website')
|
||||||
|
|
||||||
|
if (!category) {
|
||||||
|
return <div>Kategorie nicht gefunden</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Page Purpose */}
|
||||||
|
<PagePurpose
|
||||||
|
title={category.name}
|
||||||
|
purpose="Website Content & Management. Verwalten Sie Inhalte, Uebersetzungen und das CMS."
|
||||||
|
audience={['Content Manager', 'Entwickler']}
|
||||||
|
architecture={{
|
||||||
|
services: ['website (Next.js)'],
|
||||||
|
databases: [],
|
||||||
|
}}
|
||||||
|
collapsible={true}
|
||||||
|
defaultCollapsed={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modules Grid */}
|
||||||
|
<h2 className="text-lg font-semibold text-slate-900 mb-4">Module</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{category.modules.map((module) => (
|
||||||
|
<ModuleCard key={module.id} module={module} category={category} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info Section */}
|
||||||
|
<div className="mt-8 bg-sky-50 border border-sky-200 rounded-xl p-6">
|
||||||
|
<h3 className="font-semibold text-sky-800 flex items-center gap-2">
|
||||||
|
<span>🌐</span>
|
||||||
|
Website CMS
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-sky-700 mt-2">
|
||||||
|
Die BreakPilot Website wird ueber ein visuelles CMS verwaltet.
|
||||||
|
Inhalte koennen direkt bearbeitet und in mehrere Sprachen uebersetzt werden.
|
||||||
|
Aenderungen werden nach dem Speichern sofort auf der Website sichtbar.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
769
admin-v2/app/(admin)/website/uebersetzungen/page.tsx
Normal file
769
admin-v2/app/(admin)/website/uebersetzungen/page.tsx
Normal file
@@ -0,0 +1,769 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uebersetzungen - Website Content Editor
|
||||||
|
*
|
||||||
|
* Allows editing all website texts:
|
||||||
|
* - Hero Section
|
||||||
|
* - Features
|
||||||
|
* - FAQ
|
||||||
|
* - Pricing
|
||||||
|
* - Trust Indicators
|
||||||
|
* - Testimonial
|
||||||
|
*
|
||||||
|
* Includes Live-Preview of website
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
|
import { WebsiteContent, HeroContent, FeatureContent } from '@/lib/content-types'
|
||||||
|
|
||||||
|
// Admin Key (in production via login)
|
||||||
|
const ADMIN_KEY = 'breakpilot-admin-2024'
|
||||||
|
|
||||||
|
// Mapping tabs to website sections
|
||||||
|
const SECTION_MAP: Record<string, { selector: string; scrollTo: string }> = {
|
||||||
|
hero: { selector: '#hero', scrollTo: 'hero' },
|
||||||
|
features: { selector: '#features', scrollTo: 'features' },
|
||||||
|
faq: { selector: '#faq', scrollTo: 'faq' },
|
||||||
|
pricing: { selector: '#pricing', scrollTo: 'pricing' },
|
||||||
|
other: { selector: '#trust', scrollTo: 'trust' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UebersetzungenPage() {
|
||||||
|
const [content, setContent] = useState<WebsiteContent | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
||||||
|
const [activeTab, setActiveTab] = useState<'hero' | 'features' | 'faq' | 'pricing' | 'other'>('hero')
|
||||||
|
const [showPreview, setShowPreview] = useState(true)
|
||||||
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||||
|
|
||||||
|
// Scroll preview to section
|
||||||
|
const scrollToSection = useCallback((tab: string) => {
|
||||||
|
if (!iframeRef.current?.contentWindow) return
|
||||||
|
const section = SECTION_MAP[tab]
|
||||||
|
if (section) {
|
||||||
|
try {
|
||||||
|
iframeRef.current.contentWindow.postMessage(
|
||||||
|
{ type: 'scrollTo', section: section.scrollTo },
|
||||||
|
'*'
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
// Same-origin policy - fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Scroll to section on tab change
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToSection(activeTab)
|
||||||
|
}, [activeTab, scrollToSection])
|
||||||
|
|
||||||
|
// Load content
|
||||||
|
useEffect(() => {
|
||||||
|
loadContent()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function loadContent() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/website/content')
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
setContent(data)
|
||||||
|
} else {
|
||||||
|
setMessage({ type: 'error', text: 'Fehler beim Laden' })
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setMessage({ type: 'error', text: 'Fehler beim Laden' })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveChanges() {
|
||||||
|
if (!content) return
|
||||||
|
|
||||||
|
setSaving(true)
|
||||||
|
setMessage(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/website/content', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-admin-key': ADMIN_KEY,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(content),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
setMessage({ type: 'success', text: 'Gespeichert!' })
|
||||||
|
} else {
|
||||||
|
const error = await res.json()
|
||||||
|
setMessage({ type: 'error', text: error.error || 'Fehler beim Speichern' })
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setMessage({ type: 'error', text: 'Fehler beim Speichern' })
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hero Section update
|
||||||
|
function updateHero(field: keyof HeroContent, value: string) {
|
||||||
|
if (!content) return
|
||||||
|
setContent({
|
||||||
|
...content,
|
||||||
|
hero: { ...content.hero, [field]: value },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feature update
|
||||||
|
function updateFeature(index: number, field: keyof FeatureContent, value: string) {
|
||||||
|
if (!content) return
|
||||||
|
const newFeatures = [...content.features]
|
||||||
|
newFeatures[index] = { ...newFeatures[index], [field]: value }
|
||||||
|
setContent({ ...content, features: newFeatures })
|
||||||
|
}
|
||||||
|
|
||||||
|
// FAQ update
|
||||||
|
function updateFAQ(index: number, field: 'question' | 'answer', value: string | string[]) {
|
||||||
|
if (!content) return
|
||||||
|
const newFAQ = [...content.faq]
|
||||||
|
if (field === 'answer' && typeof value === 'string') {
|
||||||
|
newFAQ[index] = { ...newFAQ[index], answer: value.split('\n') }
|
||||||
|
} else if (field === 'question' && typeof value === 'string') {
|
||||||
|
newFAQ[index] = { ...newFAQ[index], question: value }
|
||||||
|
}
|
||||||
|
setContent({ ...content, faq: newFAQ })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add FAQ
|
||||||
|
function addFAQ() {
|
||||||
|
if (!content) return
|
||||||
|
setContent({
|
||||||
|
...content,
|
||||||
|
faq: [...content.faq, { question: 'Neue Frage?', answer: ['Antwort hier...'] }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove FAQ
|
||||||
|
function removeFAQ(index: number) {
|
||||||
|
if (!content) return
|
||||||
|
const newFAQ = content.faq.filter((_, i) => i !== index)
|
||||||
|
setContent({ ...content, faq: newFAQ })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pricing update
|
||||||
|
function updatePricing(index: number, field: string, value: string | number | boolean) {
|
||||||
|
if (!content) return
|
||||||
|
const newPricing = [...content.pricing]
|
||||||
|
if (field === 'price') {
|
||||||
|
newPricing[index] = { ...newPricing[index], price: Number(value) }
|
||||||
|
} else if (field === 'popular') {
|
||||||
|
newPricing[index] = { ...newPricing[index], popular: Boolean(value) }
|
||||||
|
} else if (field.startsWith('features.')) {
|
||||||
|
const subField = field.replace('features.', '')
|
||||||
|
if (subField === 'included' && typeof value === 'string') {
|
||||||
|
newPricing[index] = {
|
||||||
|
...newPricing[index],
|
||||||
|
features: {
|
||||||
|
...newPricing[index].features,
|
||||||
|
included: value.split('\n'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newPricing[index] = {
|
||||||
|
...newPricing[index],
|
||||||
|
features: {
|
||||||
|
...newPricing[index].features,
|
||||||
|
[subField]: value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newPricing[index] = { ...newPricing[index], [field]: value }
|
||||||
|
}
|
||||||
|
setContent({ ...content, pricing: newPricing })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="text-xl text-slate-600">Laden...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="text-xl text-red-600">Fehler beim Laden</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Toolbar */}
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 p-4 mb-6 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<h1 className="text-lg font-semibold text-slate-900">Uebersetzungen</h1>
|
||||||
|
{/* Preview Toggle */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPreview(!showPreview)}
|
||||||
|
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
showPreview
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||||
|
}`}
|
||||||
|
title={showPreview ? 'Preview ausblenden' : 'Preview einblenden'}
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
Live-Preview
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{message && (
|
||||||
|
<span
|
||||||
|
className={`px-3 py-1 rounded text-sm ${
|
||||||
|
message.type === 'success'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-red-100 text-red-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{message.text}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={saveChanges}
|
||||||
|
disabled={saving}
|
||||||
|
className="bg-blue-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
{saving ? 'Speichern...' : 'Speichern'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex gap-1 bg-slate-100 p-1 rounded-lg w-fit">
|
||||||
|
{(['hero', 'features', 'faq', 'pricing', 'other'] as const).map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||||
|
activeTab === tab
|
||||||
|
? 'bg-white text-slate-900 shadow-sm'
|
||||||
|
: 'text-slate-600 hover:text-slate-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab === 'hero' && 'Hero'}
|
||||||
|
{tab === 'features' && 'Features'}
|
||||||
|
{tab === 'faq' && 'FAQ'}
|
||||||
|
{tab === 'pricing' && 'Preise'}
|
||||||
|
{tab === 'other' && 'Sonstige'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Split Layout: Editor + Preview */}
|
||||||
|
<div className={`grid gap-6 ${showPreview ? 'grid-cols-2' : 'grid-cols-1'}`}>
|
||||||
|
{/* Editor Panel */}
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6 max-h-[calc(100vh-280px)] overflow-y-auto">
|
||||||
|
{/* Hero Tab */}
|
||||||
|
{activeTab === 'hero' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h2 className="text-xl font-semibold text-slate-900">Hero Section</h2>
|
||||||
|
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Badge</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={content.hero.badge}
|
||||||
|
onChange={(e) => updateHero('badge', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Titel (vor Highlight)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={content.hero.title}
|
||||||
|
onChange={(e) => updateHero('title', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Highlight 1
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={content.hero.titleHighlight1}
|
||||||
|
onChange={(e) => updateHero('titleHighlight1', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Highlight 2
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={content.hero.titleHighlight2}
|
||||||
|
onChange={(e) => updateHero('titleHighlight2', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Untertitel</label>
|
||||||
|
<textarea
|
||||||
|
value={content.hero.subtitle}
|
||||||
|
onChange={(e) => updateHero('subtitle', e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
CTA Primaer
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={content.hero.ctaPrimary}
|
||||||
|
onChange={(e) => updateHero('ctaPrimary', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
CTA Sekundaer
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={content.hero.ctaSecondary}
|
||||||
|
onChange={(e) => updateHero('ctaSecondary', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">CTA Hinweis</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={content.hero.ctaHint}
|
||||||
|
onChange={(e) => updateHero('ctaHint', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Features Tab */}
|
||||||
|
{activeTab === 'features' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h2 className="text-xl font-semibold text-slate-900">Features</h2>
|
||||||
|
|
||||||
|
{content.features.map((feature, index) => (
|
||||||
|
<div key={feature.id} className="border border-slate-200 rounded-lg p-4">
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Icon</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={feature.icon}
|
||||||
|
onChange={(e) => updateFeature(index, 'icon', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-2xl text-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Titel</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={feature.title}
|
||||||
|
onChange={(e) => updateFeature(index, 'title', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Beschreibung
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={feature.description}
|
||||||
|
onChange={(e) => updateFeature(index, 'description', e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* FAQ Tab */}
|
||||||
|
{activeTab === 'faq' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-slate-900">FAQ</h2>
|
||||||
|
<button
|
||||||
|
onClick={addFAQ}
|
||||||
|
className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors"
|
||||||
|
>
|
||||||
|
+ Frage hinzufuegen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{content.faq.map((item, index) => (
|
||||||
|
<div key={index} className="border border-slate-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex-1 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Frage {index + 1}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={item.question}
|
||||||
|
onChange={(e) => updateFAQ(index, 'question', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Antwort
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={item.answer.join('\n')}
|
||||||
|
onChange={(e) => updateFAQ(index, 'answer', e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => removeFAQ(index)}
|
||||||
|
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
|
title="Frage entfernen"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pricing Tab */}
|
||||||
|
{activeTab === 'pricing' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h2 className="text-xl font-semibold text-slate-900">Preise</h2>
|
||||||
|
|
||||||
|
{content.pricing.map((plan, index) => (
|
||||||
|
<div key={plan.id} className="border border-slate-200 rounded-lg p-4">
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={plan.name}
|
||||||
|
onChange={(e) => updatePricing(index, 'name', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Preis (EUR)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={plan.price}
|
||||||
|
onChange={(e) => updatePricing(index, 'price', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Intervall
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={plan.interval}
|
||||||
|
onChange={(e) => updatePricing(index, 'interval', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={plan.popular || false}
|
||||||
|
onChange={(e) => updatePricing(index, 'popular', e.target.checked)}
|
||||||
|
className="w-4 h-4 text-blue-600 rounded"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-slate-700">Beliebt</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Beschreibung
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={plan.description}
|
||||||
|
onChange={(e) => updatePricing(index, 'description', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Aufgaben
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={plan.features.tasks}
|
||||||
|
onChange={(e) => updatePricing(index, 'features.tasks', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Aufgaben-Beschreibung
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={plan.features.taskDescription}
|
||||||
|
onChange={(e) =>
|
||||||
|
updatePricing(index, 'features.taskDescription', e.target.value)
|
||||||
|
}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Features (eine pro Zeile)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={plan.features.included.join('\n')}
|
||||||
|
onChange={(e) => updatePricing(index, 'features.included', e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Other Tab */}
|
||||||
|
{activeTab === 'other' && (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Trust Indicators */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-slate-900 mb-4">Trust Indicators</h2>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{(['item1', 'item2', 'item3'] as const).map((key, index) => (
|
||||||
|
<div key={key} className="border border-slate-200 rounded-lg p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Wert {index + 1}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={content.trust[key].value}
|
||||||
|
onChange={(e) =>
|
||||||
|
setContent({
|
||||||
|
...content,
|
||||||
|
trust: {
|
||||||
|
...content.trust,
|
||||||
|
[key]: { ...content.trust[key], value: e.target.value },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Label {index + 1}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={content.trust[key].label}
|
||||||
|
onChange={(e) =>
|
||||||
|
setContent({
|
||||||
|
...content,
|
||||||
|
trust: {
|
||||||
|
...content.trust,
|
||||||
|
[key]: { ...content.trust[key], label: e.target.value },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Testimonial */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-slate-900 mb-4">Testimonial</h2>
|
||||||
|
<div className="border border-slate-200 rounded-lg p-4 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Zitat</label>
|
||||||
|
<textarea
|
||||||
|
value={content.testimonial.quote}
|
||||||
|
onChange={(e) =>
|
||||||
|
setContent({
|
||||||
|
...content,
|
||||||
|
testimonial: { ...content.testimonial, quote: e.target.value },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Autor</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={content.testimonial.author}
|
||||||
|
onChange={(e) =>
|
||||||
|
setContent({
|
||||||
|
...content,
|
||||||
|
testimonial: { ...content.testimonial, author: e.target.value },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Rolle</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={content.testimonial.role}
|
||||||
|
onChange={(e) =>
|
||||||
|
setContent({
|
||||||
|
...content,
|
||||||
|
testimonial: { ...content.testimonial, role: e.target.value },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Live Preview Panel */}
|
||||||
|
{showPreview && (
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
||||||
|
{/* Preview Header */}
|
||||||
|
<div className="bg-slate-50 border-b border-slate-200 px-4 py-3 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-red-400"></div>
|
||||||
|
<div className="w-3 h-3 rounded-full bg-yellow-400"></div>
|
||||||
|
<div className="w-3 h-3 rounded-full bg-green-400"></div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-slate-500 ml-2">breakpilot.app</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-medium text-slate-600 bg-slate-200 px-2 py-1 rounded">
|
||||||
|
{activeTab === 'hero' && 'Hero Section'}
|
||||||
|
{activeTab === 'features' && 'Features'}
|
||||||
|
{activeTab === 'faq' && 'FAQ'}
|
||||||
|
{activeTab === 'pricing' && 'Pricing'}
|
||||||
|
{activeTab === 'other' && 'Trust & Testimonial'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => iframeRef.current?.contentWindow?.location.reload()}
|
||||||
|
className="p-1.5 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded transition-colors"
|
||||||
|
title="Preview neu laden"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Preview Frame */}
|
||||||
|
<div className="relative h-[calc(100vh-340px)] bg-slate-100">
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
src={`https://macmini:3000/?preview=true§ion=${activeTab}#${activeTab}`}
|
||||||
|
className="w-full h-full border-0 scale-75 origin-top-left"
|
||||||
|
style={{
|
||||||
|
width: '133.33%',
|
||||||
|
height: '133.33%',
|
||||||
|
transform: 'scale(0.75)',
|
||||||
|
transformOrigin: 'top left',
|
||||||
|
}}
|
||||||
|
title="Website Preview"
|
||||||
|
sandbox="allow-same-origin allow-scripts"
|
||||||
|
/>
|
||||||
|
{/* Section Indicator */}
|
||||||
|
<div className="absolute bottom-4 left-4 right-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 text-sm">
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
Du bearbeitest: <strong>
|
||||||
|
{activeTab === 'hero' && 'Hero Section (Startbereich)'}
|
||||||
|
{activeTab === 'features' && 'Features (Funktionen)'}
|
||||||
|
{activeTab === 'faq' && 'FAQ (Haeufige Fragen)'}
|
||||||
|
{activeTab === 'pricing' && 'Pricing (Preise)'}
|
||||||
|
{activeTab === 'other' && 'Trust & Testimonial'}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
65
admin-v2/app/api/website/content/route.ts
Normal file
65
admin-v2/app/api/website/content/route.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Website Content API Route
|
||||||
|
*
|
||||||
|
* GET: Load current website content
|
||||||
|
* POST: Save changed content (Admin only)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getContent, saveContent } from '@/lib/content'
|
||||||
|
import type { WebsiteContent } from '@/lib/content-types'
|
||||||
|
|
||||||
|
// GET - Load content
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const content = getContent()
|
||||||
|
return NextResponse.json(content)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading content:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to load content' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST - Save content
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const adminKey = request.headers.get('x-admin-key')
|
||||||
|
const expectedKey = process.env.ADMIN_API_KEY || 'breakpilot-admin-2024'
|
||||||
|
|
||||||
|
if (adminKey !== expectedKey) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Unauthorized' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const content: WebsiteContent = await request.json()
|
||||||
|
|
||||||
|
if (!content.hero || !content.features || !content.faq || !content.pricing) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Invalid content structure' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = saveContent(content)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return NextResponse.json({ success: true, message: 'Content saved' })
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: result.error || 'Failed to save content' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving content:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error instanceof Error ? error.message : 'Failed to save content' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
52
admin-v2/app/api/website/status/route.ts
Normal file
52
admin-v2/app/api/website/status/route.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Website Status API Route
|
||||||
|
*
|
||||||
|
* GET: Health-Check ob die Website (Port 3000) erreichbar ist
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
const WEBSITE_URL = process.env.WEBSITE_URL || 'http://website:3000'
|
||||||
|
const WEBSITE_FALLBACK_URL = 'https://macmini:3000'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const start = Date.now()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 5000)
|
||||||
|
|
||||||
|
let response: Response | null = null
|
||||||
|
try {
|
||||||
|
response = await fetch(WEBSITE_URL, {
|
||||||
|
method: 'HEAD',
|
||||||
|
signal: controller.signal,
|
||||||
|
cache: 'no-store',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// Docker-internal name failed, try fallback
|
||||||
|
response = await fetch(WEBSITE_FALLBACK_URL, {
|
||||||
|
method: 'HEAD',
|
||||||
|
signal: controller.signal,
|
||||||
|
cache: 'no-store',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseTime = Date.now() - start
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
online: response.ok || response.status < 500,
|
||||||
|
responseTime,
|
||||||
|
statusCode: response.status,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
const responseTime = Date.now() - start
|
||||||
|
return NextResponse.json({
|
||||||
|
online: false,
|
||||||
|
responseTime,
|
||||||
|
error: error instanceof Error ? error.message : 'Website nicht erreichbar',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
540
website/app/foerderantrag/[applicationId]/page.tsx
Normal file
540
website/app/foerderantrag/[applicationId]/page.tsx
Normal file
@@ -0,0 +1,540 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useParams, useRouter } from 'next/navigation'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useLanguage } from '@/lib/LanguageContext'
|
||||||
|
|
||||||
|
interface WizardStep {
|
||||||
|
number: number
|
||||||
|
id: string
|
||||||
|
titleKey: string
|
||||||
|
subtitleKey: string
|
||||||
|
descKey: string
|
||||||
|
icon: string
|
||||||
|
is_required: boolean
|
||||||
|
is_completed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepIcons: Record<string, React.ReactNode> = {
|
||||||
|
'document-text': (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
'academic-cap': (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 14l9-5-9-5-9 5 9 5z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
'server': (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
'document-report': (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
'currency-euro': (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.121 15.536c-1.171 1.952-3.07 1.952-4.242 0-1.172-1.953-1.172-5.119 0-7.072 1.171-1.952 3.07-1.952 4.242 0M8 10.5h4m-4 3h4m9-1.5a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
'calculator': (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
'calendar': (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
'document-download': (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FoerderantragWizardPage() {
|
||||||
|
const params = useParams()
|
||||||
|
const router = useRouter()
|
||||||
|
const { t, isRTL } = useLanguage()
|
||||||
|
const applicationId = params.applicationId as string
|
||||||
|
|
||||||
|
const defaultSteps: WizardStep[] = [
|
||||||
|
{ number: 1, id: 'foerderprogramm', titleKey: 'fa_step1_title', subtitleKey: 'fa_step1_subtitle', descKey: 'fa_step1_desc', icon: 'document-text', is_required: true, is_completed: false },
|
||||||
|
{ number: 2, id: 'schulinformationen', titleKey: 'fa_step2_title', subtitleKey: 'fa_step2_subtitle', descKey: 'fa_step2_desc', icon: 'academic-cap', is_required: true, is_completed: false },
|
||||||
|
{ number: 3, id: 'bestandsaufnahme', titleKey: 'fa_step3_title', subtitleKey: 'fa_step3_subtitle', descKey: 'fa_step3_desc', icon: 'server', is_required: true, is_completed: false },
|
||||||
|
{ number: 4, id: 'projektbeschreibung', titleKey: 'fa_step4_title', subtitleKey: 'fa_step4_subtitle', descKey: 'fa_step4_desc', icon: 'document-report', is_required: true, is_completed: false },
|
||||||
|
{ number: 5, id: 'investitionen', titleKey: 'fa_step5_title', subtitleKey: 'fa_step5_subtitle', descKey: 'fa_step5_desc', icon: 'currency-euro', is_required: true, is_completed: false },
|
||||||
|
{ number: 6, id: 'finanzierungsplan', titleKey: 'fa_step6_title', subtitleKey: 'fa_step6_subtitle', descKey: 'fa_step6_desc', icon: 'calculator', is_required: true, is_completed: false },
|
||||||
|
{ number: 7, id: 'zeitplan', titleKey: 'fa_step7_title', subtitleKey: 'fa_step7_subtitle', descKey: 'fa_step7_desc', icon: 'calendar', is_required: true, is_completed: false },
|
||||||
|
{ number: 8, id: 'abschluss', titleKey: 'fa_step8_title', subtitleKey: 'fa_step8_subtitle', descKey: 'fa_step8_desc', icon: 'document-download', is_required: true, is_completed: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
const [currentStep, setCurrentStep] = useState(1)
|
||||||
|
const [steps, setSteps] = useState<WizardStep[]>(defaultSteps)
|
||||||
|
const [formData, setFormData] = useState<FormData>({})
|
||||||
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
|
const [showAssistant, setShowAssistant] = useState(false)
|
||||||
|
const [assistantMessage, setAssistantMessage] = useState('')
|
||||||
|
const [assistantHistory, setAssistantHistory] = useState<{ role: string; content: string }[]>([])
|
||||||
|
const [isDemo, setIsDemo] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (applicationId.startsWith('demo-')) {
|
||||||
|
setIsDemo(true)
|
||||||
|
}
|
||||||
|
}, [applicationId])
|
||||||
|
|
||||||
|
const handleFieldChange = (fieldId: string, value: any) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[`step_${currentStep}`]: {
|
||||||
|
...prev[`step_${currentStep}`],
|
||||||
|
[fieldId]: value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveStep = async () => {
|
||||||
|
setIsSaving(true)
|
||||||
|
try {
|
||||||
|
setSteps(prev => prev.map(s =>
|
||||||
|
s.number === currentStep ? { ...s, is_completed: true } : s
|
||||||
|
))
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNextStep = async () => {
|
||||||
|
await handleSaveStep()
|
||||||
|
if (currentStep < 8) {
|
||||||
|
setCurrentStep(prev => prev + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePrevStep = () => {
|
||||||
|
if (currentStep > 1) {
|
||||||
|
setCurrentStep(prev => prev - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAskAssistant = async () => {
|
||||||
|
if (!assistantMessage.trim()) return
|
||||||
|
const userMessage = assistantMessage
|
||||||
|
setAssistantMessage('')
|
||||||
|
setAssistantHistory(prev => [...prev, { role: 'user', content: userMessage }])
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const response = `${t('fa_assistant_title')}: ${userMessage}`
|
||||||
|
setAssistantHistory(prev => [...prev, { role: 'assistant', content: response }])
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStepData = steps.find(s => s.number === currentStep)
|
||||||
|
|
||||||
|
const renderStepContent = () => {
|
||||||
|
switch (currentStep) {
|
||||||
|
case 1:
|
||||||
|
return <Step1 t={t} />
|
||||||
|
case 2:
|
||||||
|
return <Step2 t={t} />
|
||||||
|
case 3:
|
||||||
|
return <Step3 t={t} />
|
||||||
|
case 4:
|
||||||
|
return <Step4 t={t} />
|
||||||
|
case 5:
|
||||||
|
return <Step5 t={t} />
|
||||||
|
case 6:
|
||||||
|
return <Step6 t={t} />
|
||||||
|
case 7:
|
||||||
|
return <Step7 t={t} />
|
||||||
|
case 8:
|
||||||
|
return <Step8 t={t} />
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`min-h-screen bg-slate-50 ${isRTL ? 'rtl' : ''}`}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-white border-b border-slate-200 sticky top-0 z-20">
|
||||||
|
<div className="px-6 py-4">
|
||||||
|
<div className={`flex items-center justify-between ${isRTL ? 'flex-row-reverse' : ''}`}>
|
||||||
|
<div className={`flex items-center gap-4 ${isRTL ? 'flex-row-reverse' : ''}`}>
|
||||||
|
<Link
|
||||||
|
href="/foerderantrag"
|
||||||
|
className="p-2 rounded-lg hover:bg-slate-100 transition-colors"
|
||||||
|
>
|
||||||
|
<svg className={`w-5 h-5 text-slate-600 ${isRTL ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
<div>
|
||||||
|
<h1 className="font-semibold text-slate-900">{t('fa_wizard_header')}</h1>
|
||||||
|
<p className="text-sm text-slate-500">
|
||||||
|
{t('fa_step1_title').split('')[0] && `${currentStep} ${t('fa_wizard_step_of')} ${steps.length}: ${currentStepData ? t(currentStepData.titleKey) : ''}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`flex items-center gap-3 ${isRTL ? 'flex-row-reverse' : ''}`}>
|
||||||
|
{isDemo && (
|
||||||
|
<span className="px-3 py-1 bg-amber-100 text-amber-700 text-sm font-medium rounded-full">
|
||||||
|
{t('fa_demo_mode')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAssistant(!showAssistant)}
|
||||||
|
className={`p-2 rounded-lg transition-colors ${showAssistant ? 'bg-blue-100 text-blue-600' : 'hover:bg-slate-100 text-slate-600'}`}
|
||||||
|
title={t('fa_assistant_title')}
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSaveStep}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg font-medium hover:bg-slate-200 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
{isSaving ? t('fa_wizard_saving') : t('fa_wizard_save')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Steps */}
|
||||||
|
<div className="px-6 pb-4 overflow-x-auto">
|
||||||
|
<div className={`flex gap-1 min-w-max ${isRTL ? 'flex-row-reverse' : ''}`}>
|
||||||
|
{steps.map((step) => (
|
||||||
|
<button
|
||||||
|
key={step.number}
|
||||||
|
onClick={() => setCurrentStep(step.number)}
|
||||||
|
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-all ${
|
||||||
|
currentStep === step.number
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: step.is_completed
|
||||||
|
? 'bg-green-100 text-green-700 hover:bg-green-200'
|
||||||
|
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${
|
||||||
|
currentStep === step.number
|
||||||
|
? 'bg-white/20'
|
||||||
|
: step.is_completed
|
||||||
|
? 'bg-green-500 text-white'
|
||||||
|
: 'bg-slate-300 text-slate-600'
|
||||||
|
}`}>
|
||||||
|
{step.is_completed && currentStep !== step.number ? (
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
step.number
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="hidden md:block font-medium">{t(step.titleKey)}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex">
|
||||||
|
{/* Form Area */}
|
||||||
|
<div className={`flex-1 p-6 transition-all ${showAssistant ? (isRTL ? 'pl-96' : 'pr-96') : ''}`}>
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
{/* Step Header */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className={`flex items-center gap-3 mb-2 ${isRTL ? 'flex-row-reverse' : ''}`}>
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-blue-100 text-blue-600 flex items-center justify-center">
|
||||||
|
{stepIcons[currentStepData?.icon || 'document-text']}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-slate-900">
|
||||||
|
{currentStepData ? t(currentStepData.titleKey) : ''}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-slate-500">
|
||||||
|
{currentStepData ? t(currentStepData.descKey) : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step Content */}
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||||
|
{renderStepContent()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className={`flex items-center justify-between mt-6 ${isRTL ? 'flex-row-reverse' : ''}`}>
|
||||||
|
<button
|
||||||
|
onClick={handlePrevStep}
|
||||||
|
disabled={currentStep === 1}
|
||||||
|
className={`px-6 py-3 text-slate-600 hover:text-slate-900 disabled:opacity-50 disabled:cursor-not-allowed font-medium flex items-center gap-2 ${isRTL ? 'flex-row-reverse' : ''}`}
|
||||||
|
>
|
||||||
|
<svg className={`w-4 h-4 ${isRTL ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
{t('fa_wizard_prev')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleNextStep}
|
||||||
|
className={`px-6 py-3 bg-blue-600 text-white rounded-xl font-semibold hover:bg-blue-700 flex items-center gap-2 transition-colors ${isRTL ? 'flex-row-reverse' : ''}`}
|
||||||
|
>
|
||||||
|
{currentStep === 8 ? t('fa_wizard_finish') : t('fa_wizard_next')}
|
||||||
|
<svg className={`w-4 h-4 ${isRTL ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Assistant Sidebar */}
|
||||||
|
{showAssistant && (
|
||||||
|
<div className={`fixed ${isRTL ? 'left-0' : 'right-0'} top-0 h-full w-96 bg-white border-${isRTL ? 'r' : 'l'} border-slate-200 shadow-xl z-30 flex flex-col`}>
|
||||||
|
<div className={`p-4 border-b border-slate-200 flex items-center justify-between ${isRTL ? 'flex-row-reverse' : ''}`}>
|
||||||
|
<div className={`flex items-center gap-2 ${isRTL ? 'flex-row-reverse' : ''}`}>
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center">
|
||||||
|
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-slate-900">{t('fa_assistant_title')}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAssistant(false)}
|
||||||
|
className="p-2 rounded-lg hover:bg-slate-100 transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chat History */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
|
{assistantHistory.length === 0 && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p className="text-slate-500 text-sm">
|
||||||
|
{t('fa_assistant_placeholder')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{assistantHistory.map((msg, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`max-w-[85%] p-3 rounded-xl text-sm ${
|
||||||
|
msg.role === 'user'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-slate-100 text-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{msg.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<div className="p-4 border-t border-slate-200">
|
||||||
|
<div className={`flex gap-2 ${isRTL ? 'flex-row-reverse' : ''}`}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={assistantMessage}
|
||||||
|
onChange={(e) => setAssistantMessage(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleAskAssistant()}
|
||||||
|
placeholder={t('fa_assistant_placeholder')}
|
||||||
|
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleAskAssistant}
|
||||||
|
className="p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step Components
|
||||||
|
function Step1({ t }: { t: (key: string) => string }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<p className="text-slate-600">{t('fa_step1_desc')}</p>
|
||||||
|
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<p className="text-sm text-blue-700">{t('fa_wizard_next')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Step2({ t }: { t: (key: string) => string }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">{t('fa_step2_title')} *</label>
|
||||||
|
<input type="text" className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">{t('fa_step2_subtitle')}</label>
|
||||||
|
<input type="number" className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">{t('fa_step2_subtitle')}</label>
|
||||||
|
<input type="number" className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Step3({ t }: { t: (key: string) => string }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">{t('fa_step3_desc')}</label>
|
||||||
|
<select className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
<option>16 Mbit/s</option>
|
||||||
|
<option>16-50 Mbit/s</option>
|
||||||
|
<option>50-100 Mbit/s</option>
|
||||||
|
<option>100-250 Mbit/s</option>
|
||||||
|
<option>250+ Mbit/s</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Step4({ t }: { t: (key: string) => string }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">{t('fa_step4_desc')} *</label>
|
||||||
|
<textarea className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" rows={3} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">{t('fa_step4_subtitle')} *</label>
|
||||||
|
<textarea className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" rows={4} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Step5({ t }: { t: (key: string) => string }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<p className="text-slate-600">{t('fa_step5_desc')}</p>
|
||||||
|
<div className="border border-slate-200 rounded-lg overflow-hidden">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="bg-slate-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-2 text-left font-medium text-slate-700">{t('fa_step5_subtitle')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr className="border-t border-slate-200">
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<button className="text-blue-600 hover:text-blue-700 font-medium text-sm flex items-center gap-1">
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Step6({ t }: { t: (key: string) => string }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">{t('fa_step6_desc')}</label>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<input type="range" min="50" max="100" defaultValue="90" className="flex-1" />
|
||||||
|
<span className="text-lg font-semibold text-slate-900">90%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="p-4 bg-slate-50 rounded-lg">
|
||||||
|
<div className="text-sm text-slate-500">{t('fa_step6_subtitle')}</div>
|
||||||
|
<div className="text-xl font-bold text-slate-900">0,00 EUR</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-blue-50 rounded-lg">
|
||||||
|
<div className="text-sm text-blue-600">{t('fa_step6_title')}</div>
|
||||||
|
<div className="text-xl font-bold text-blue-700">0,00 EUR</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Step7({ t }: { t: (key: string) => string }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">{t('fa_step7_subtitle')} *</label>
|
||||||
|
<input type="date" className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">{t('fa_step7_subtitle')} *</label>
|
||||||
|
<input type="date" className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Step8({ t }: { t: (key: string) => string }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||||
|
<h3 className="font-semibold text-green-800">{t('fa_step8_title')}</h3>
|
||||||
|
<p className="text-sm text-green-700 mt-1">{t('fa_step8_desc')}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">{t('fa_step8_subtitle')} *</label>
|
||||||
|
<textarea className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" rows={4} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="flex items-center gap-3">
|
||||||
|
<input type="checkbox" className="w-4 h-4 rounded border-slate-300" />
|
||||||
|
<span className="text-sm text-slate-700">{t('fa_info_text')}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
312
website/app/foerderantrag/new/page.tsx
Normal file
312
website/app/foerderantrag/new/page.tsx
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Suspense, useState, useEffect } from 'react'
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import Header from '@/components/Header'
|
||||||
|
import Footer from '@/components/Footer'
|
||||||
|
import { useLanguage } from '@/lib/LanguageContext'
|
||||||
|
|
||||||
|
type FundingProgram = 'DIGITALPAKT_1' | 'DIGITALPAKT_2' | 'LANDESFOERDERUNG' | 'SCHULTRAEGER'
|
||||||
|
type FederalState = 'NI' | 'NRW' | 'BAY' | 'BW' | 'HE' | 'SN' | 'TH' | 'SA' | 'BB' | 'MV' | 'SH' | 'HH' | 'HB' | 'BE' | 'SL' | 'RP'
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
title: string
|
||||||
|
funding_program: FundingProgram
|
||||||
|
federal_state: FederalState
|
||||||
|
preset_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const federalStates: { value: string; label: string }[] = [
|
||||||
|
{ value: 'NI', label: 'Niedersachsen' },
|
||||||
|
{ value: 'NRW', label: 'Nordrhein-Westfalen' },
|
||||||
|
{ value: 'BAY', label: 'Bayern' },
|
||||||
|
{ value: 'BW', label: 'Baden-Wuerttemberg' },
|
||||||
|
{ value: 'HE', label: 'Hessen' },
|
||||||
|
{ value: 'SN', label: 'Sachsen' },
|
||||||
|
{ value: 'TH', label: 'Thueringen' },
|
||||||
|
{ value: 'SA', label: 'Sachsen-Anhalt' },
|
||||||
|
{ value: 'BB', label: 'Brandenburg' },
|
||||||
|
{ value: 'MV', label: 'Mecklenburg-Vorpommern' },
|
||||||
|
{ value: 'SH', label: 'Schleswig-Holstein' },
|
||||||
|
{ value: 'HH', label: 'Hamburg' },
|
||||||
|
{ value: 'HB', label: 'Bremen' },
|
||||||
|
{ value: 'BE', label: 'Berlin' },
|
||||||
|
{ value: 'SL', label: 'Saarland' },
|
||||||
|
{ value: 'RP', label: 'Rheinland-Pfalz' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function NewFoerderantragPage() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="min-h-screen bg-slate-50 flex items-center justify-center"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div></div>}>
|
||||||
|
<NewFoerderantragContent />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NewFoerderantragContent() {
|
||||||
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const { t, isRTL } = useLanguage()
|
||||||
|
|
||||||
|
const presets = [
|
||||||
|
{ id: 'breakpilot_basic', nameKey: 'fa_preset_basic_name', descKey: 'fa_preset_basic_desc', budgetKey: 'fa_preset_basic_budget', color: 'blue' },
|
||||||
|
{ id: 'breakpilot_cluster', nameKey: 'fa_preset_cluster_name', descKey: 'fa_preset_cluster_desc', budgetKey: 'fa_preset_cluster_budget', color: 'purple' },
|
||||||
|
{ id: '', nameKey: 'fa_preset_custom_name', descKey: 'fa_preset_custom_desc', budgetKey: 'fa_preset_custom_budget', color: 'slate' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const fundingPrograms = [
|
||||||
|
{ value: 'DIGITALPAKT_2', labelKey: 'fa_program_dp2' },
|
||||||
|
{ value: 'DIGITALPAKT_1', labelKey: 'fa_program_dp1' },
|
||||||
|
{ value: 'LANDESFOERDERUNG', labelKey: 'fa_program_landes' },
|
||||||
|
{ value: 'SCHULTRAEGER', labelKey: 'fa_program_traeger' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<FormData>({
|
||||||
|
title: '',
|
||||||
|
funding_program: 'DIGITALPAKT_2',
|
||||||
|
federal_state: 'NI',
|
||||||
|
preset_id: searchParams.get('preset') || '',
|
||||||
|
})
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const preset = searchParams.get('preset')
|
||||||
|
if (preset) {
|
||||||
|
const presetInfo = presets.find(p => p.id === preset)
|
||||||
|
if (presetInfo && presetInfo.id) {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
preset_id: preset,
|
||||||
|
title: `${t(presetInfo.nameKey)} - ${new Date().toLocaleDateString('de-DE')}`,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [searchParams])
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
if (!formData.title.trim()) {
|
||||||
|
setError(t('fa_project_title_label'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true)
|
||||||
|
try {
|
||||||
|
const mockId = 'demo-' + Date.now()
|
||||||
|
router.push(`/foerderantrag/${mockId}`)
|
||||||
|
} catch {
|
||||||
|
setError('Error')
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPresetColorClasses = (color: string, isSelected: boolean) => {
|
||||||
|
const colors: Record<string, { border: string; bg: string; ring: string }> = {
|
||||||
|
blue: { border: isSelected ? 'border-blue-500' : 'border-slate-200', bg: isSelected ? 'bg-blue-50' : 'bg-white', ring: 'ring-blue-500' },
|
||||||
|
purple: { border: isSelected ? 'border-purple-500' : 'border-slate-200', bg: isSelected ? 'bg-purple-50' : 'bg-white', ring: 'ring-purple-500' },
|
||||||
|
slate: { border: isSelected ? 'border-slate-500' : 'border-slate-200', bg: isSelected ? 'bg-slate-50' : 'bg-white', ring: 'ring-slate-500' },
|
||||||
|
}
|
||||||
|
return colors[color] || colors.slate
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<main className={`min-h-screen bg-slate-50 pt-20 ${isRTL ? 'rtl' : ''}`}>
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
{/* Back Link */}
|
||||||
|
<Link
|
||||||
|
href="/foerderantrag"
|
||||||
|
className={`inline-flex items-center gap-2 text-slate-600 hover:text-slate-900 mb-6 ${isRTL ? 'flex-row-reverse' : ''}`}
|
||||||
|
>
|
||||||
|
<svg className={`w-4 h-4 ${isRTL ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
{t('fa_back_overview')}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-slate-900">{t('fa_new_title')}</h1>
|
||||||
|
<p className="mt-2 text-slate-600">{t('fa_new_subtitle')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-8">
|
||||||
|
{/* Preset Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-3">
|
||||||
|
{t('fa_preset_label')}
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{presets.map((preset) => {
|
||||||
|
const isSelected = formData.preset_id === preset.id
|
||||||
|
const colors = getPresetColorClasses(preset.color, isSelected)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={preset.id || 'custom'}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
preset_id: preset.id,
|
||||||
|
title: preset.id ? `${t(preset.nameKey)} - ${new Date().toLocaleDateString('de-DE')}` : prev.title,
|
||||||
|
}))}
|
||||||
|
className={`relative p-4 rounded-xl border-2 text-left transition-all ${colors.border} ${colors.bg} ${isSelected ? 'ring-2 ' + colors.ring : ''}`}
|
||||||
|
>
|
||||||
|
{isSelected && (
|
||||||
|
<div className={`absolute top-2 ${isRTL ? 'left-2' : 'right-2'}`}>
|
||||||
|
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h3 className="font-semibold text-slate-900">{t(preset.nameKey)}</h3>
|
||||||
|
<p className="text-sm text-slate-500 mt-1">{t(preset.descKey)}</p>
|
||||||
|
<p className="text-sm font-medium text-slate-700 mt-2">{t(preset.budgetKey)}</p>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Funding Program */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-3">
|
||||||
|
{t('fa_program_label')} *
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{fundingPrograms.map((program) => (
|
||||||
|
<label
|
||||||
|
key={program.value}
|
||||||
|
className={`relative flex items-start p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
||||||
|
formData.funding_program === program.value
|
||||||
|
? 'border-blue-500 bg-blue-50 ring-2 ring-blue-500'
|
||||||
|
: 'border-slate-200 bg-white hover:border-slate-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="funding_program"
|
||||||
|
value={program.value}
|
||||||
|
checked={formData.funding_program === program.value}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, funding_program: e.target.value as FundingProgram }))}
|
||||||
|
className="sr-only"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-slate-900">{t(program.labelKey)}</span>
|
||||||
|
{formData.funding_program === program.value && (
|
||||||
|
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Federal State */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-3">
|
||||||
|
{t('fa_state_label')} *
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||||
|
{federalStates.map((state) => (
|
||||||
|
<button
|
||||||
|
key={state.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFormData(prev => ({ ...prev, federal_state: state.value as FederalState }))}
|
||||||
|
className={`px-4 py-3 rounded-lg border-2 text-sm font-medium transition-all ${
|
||||||
|
formData.federal_state === state.value
|
||||||
|
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||||||
|
: 'border-slate-200 bg-white text-slate-700 hover:border-slate-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{state.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{formData.federal_state === 'NI' && (
|
||||||
|
<p className="mt-2 text-sm text-slate-500">{t('fa_ni_hint')}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Project Title */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="title" className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
{t('fa_project_title_label')} *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="title"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
||||||
|
placeholder={t('fa_project_title_label')}
|
||||||
|
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
maxLength={200}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error */}
|
||||||
|
{error && (
|
||||||
|
<div className="p-4 bg-red-50 border border-red-200 rounded-xl text-red-700">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className={`flex items-center justify-between pt-4 border-t border-slate-200 ${isRTL ? 'flex-row-reverse' : ''}`}>
|
||||||
|
<Link
|
||||||
|
href="/foerderantrag"
|
||||||
|
className="px-6 py-3 text-slate-600 hover:text-slate-900 font-medium"
|
||||||
|
>
|
||||||
|
{t('fa_cancel')}
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={`px-8 py-3 bg-blue-600 text-white rounded-xl font-semibold hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 transition-colors ${isRTL ? 'flex-row-reverse' : ''}`}
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||||
|
{t('fa_wizard_saving')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{t('fa_submit')}
|
||||||
|
<svg className={`w-4 h-4 ${isRTL ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||||
|
</svg>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Help Box */}
|
||||||
|
<div className="mt-8 bg-amber-50 border border-amber-200 rounded-xl p-6">
|
||||||
|
<div className={`flex gap-4 ${isRTL ? 'flex-row-reverse' : ''}`}>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<svg className="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-amber-800">{t('fa_ai_hint_title')}</h3>
|
||||||
|
<p className="mt-1 text-sm text-amber-700">{t('fa_ai_hint_text')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
256
website/app/foerderantrag/page.tsx
Normal file
256
website/app/foerderantrag/page.tsx
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import Header from '@/components/Header'
|
||||||
|
import Footer from '@/components/Footer'
|
||||||
|
import { useLanguage } from '@/lib/LanguageContext'
|
||||||
|
|
||||||
|
interface FundingApplication {
|
||||||
|
id: string
|
||||||
|
application_number: string
|
||||||
|
title: string
|
||||||
|
funding_program: string
|
||||||
|
status: string
|
||||||
|
current_step: number
|
||||||
|
total_steps: number
|
||||||
|
requested_amount: number
|
||||||
|
school_profile?: {
|
||||||
|
name: string
|
||||||
|
federal_state: string
|
||||||
|
}
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Statistics {
|
||||||
|
total_applications: number
|
||||||
|
draft_count: number
|
||||||
|
submitted_count: number
|
||||||
|
approved_count: number
|
||||||
|
total_requested: number
|
||||||
|
total_approved: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FoerderantragPage() {
|
||||||
|
const { t, isRTL } = useLanguage()
|
||||||
|
const [applications, setApplications] = useState<FundingApplication[]>([])
|
||||||
|
const [statistics, setStatistics] = useState<Statistics | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setApplications([])
|
||||||
|
setStatistics({
|
||||||
|
total_applications: 0,
|
||||||
|
draft_count: 0,
|
||||||
|
submitted_count: 0,
|
||||||
|
approved_count: 0,
|
||||||
|
total_requested: 0,
|
||||||
|
total_approved: 0,
|
||||||
|
})
|
||||||
|
setLoading(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const formatCurrency = (amount: number) => {
|
||||||
|
return new Intl.NumberFormat('de-DE', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'EUR',
|
||||||
|
}).format(amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<main className={`min-h-screen bg-slate-50 pt-20 ${isRTL ? 'rtl' : ''}`}>
|
||||||
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-800 p-8 text-white">
|
||||||
|
<div className="absolute inset-0 bg-[url('/grid-pattern.svg')] opacity-10" />
|
||||||
|
<div className="relative z-10">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">{t('fa_title')}</h1>
|
||||||
|
<p className="mt-2 text-blue-100 max-w-2xl">
|
||||||
|
{t('fa_subtitle')}
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 flex gap-4">
|
||||||
|
<Link
|
||||||
|
href="/foerderantrag/new"
|
||||||
|
className={`inline-flex items-center gap-2 px-6 py-3 bg-white text-blue-700 rounded-xl font-semibold hover:bg-blue-50 transition-colors shadow-lg ${isRTL ? 'flex-row-reverse' : ''}`}
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
{t('fa_new_application')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<svg className="w-32 h-32 text-blue-300 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Statistics Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 p-5 hover:shadow-md transition-shadow">
|
||||||
|
<div className={`flex items-center gap-3 ${isRTL ? 'flex-row-reverse' : ''}`}>
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-blue-100 flex items-center justify-center">
|
||||||
|
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-2xl font-bold text-slate-900">{statistics?.total_applications || 0}</div>
|
||||||
|
<div className="text-sm text-slate-500">{t('fa_statistics')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 p-5 hover:shadow-md transition-shadow">
|
||||||
|
<div className={`flex items-center gap-3 ${isRTL ? 'flex-row-reverse' : ''}`}>
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-green-100 flex items-center justify-center">
|
||||||
|
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-2xl font-bold text-slate-900">{formatCurrency(statistics?.total_requested || 0)}</div>
|
||||||
|
<div className="text-sm text-slate-500">{t('fa_program_dp2')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Start Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<Link
|
||||||
|
href="/foerderantrag/new?preset=breakpilot_basic"
|
||||||
|
className="group bg-white rounded-xl border-2 border-slate-200 p-6 hover:border-blue-400 hover:shadow-lg transition-all"
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center mb-4">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-lg text-slate-900 group-hover:text-blue-600 transition-colors">
|
||||||
|
{t('fa_preset_basic_name')}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-slate-500 mt-1">
|
||||||
|
{t('fa_preset_basic_desc')}
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 text-sm font-medium text-blue-600">
|
||||||
|
{t('fa_preset_basic_budget')}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/foerderantrag/new?preset=breakpilot_cluster"
|
||||||
|
className="group bg-white rounded-xl border-2 border-slate-200 p-6 hover:border-purple-400 hover:shadow-lg transition-all"
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-purple-500 to-pink-600 flex items-center justify-center mb-4">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-lg text-slate-900 group-hover:text-purple-600 transition-colors">
|
||||||
|
{t('fa_preset_cluster_name')}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-slate-500 mt-1">
|
||||||
|
{t('fa_preset_cluster_desc')}
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 text-sm font-medium text-purple-600">
|
||||||
|
{t('fa_preset_cluster_budget')}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/foerderantrag/new"
|
||||||
|
className="group bg-white rounded-xl border-2 border-slate-200 p-6 hover:border-slate-400 hover:shadow-lg transition-all"
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-slate-500 to-slate-700 flex items-center justify-center mb-4">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-lg text-slate-900 group-hover:text-slate-700 transition-colors">
|
||||||
|
{t('fa_preset_custom_name')}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-slate-500 mt-1">
|
||||||
|
{t('fa_preset_custom_desc')}
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 text-sm font-medium text-slate-600">
|
||||||
|
{t('fa_preset_custom_budget')}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Applications List */}
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||||
|
{loading ? (
|
||||||
|
<div className="p-12 text-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
) : applications.length === 0 ? (
|
||||||
|
<div className="p-12 text-center">
|
||||||
|
<svg className="w-16 h-16 text-slate-300 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
<h3 className="mt-4 text-lg font-medium text-slate-900">{t('fa_no_applications')}</h3>
|
||||||
|
<p className="mt-2 text-slate-500">{t('fa_start_first')}</p>
|
||||||
|
<Link
|
||||||
|
href="/foerderantrag/new"
|
||||||
|
className="mt-6 inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
{t('fa_new_application')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y divide-slate-100">
|
||||||
|
{applications.map((app) => (
|
||||||
|
<Link
|
||||||
|
key={app.id}
|
||||||
|
href={`/foerderantrag/${app.id}`}
|
||||||
|
className={`flex items-center gap-4 p-4 hover:bg-slate-50 transition-colors ${isRTL ? 'flex-row-reverse' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-medium text-slate-900 truncate">{app.title}</h3>
|
||||||
|
<div className="text-sm text-slate-500 mt-1">{app.application_number}</div>
|
||||||
|
</div>
|
||||||
|
<div className={isRTL ? 'text-left' : 'text-right'}>
|
||||||
|
<div className="font-medium text-slate-900">{formatCurrency(app.requested_amount)}</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info Box */}
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6">
|
||||||
|
<div className={`flex gap-4 ${isRTL ? 'flex-row-reverse' : ''}`}>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-blue-800">{t('fa_info_title')}</h3>
|
||||||
|
<p className="mt-1 text-sm text-blue-700">{t('fa_info_text')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -29,6 +29,9 @@ export default function Header() {
|
|||||||
<Link href="/#features" className="text-slate-600 hover:text-slate-900 transition-colors">
|
<Link href="/#features" className="text-slate-600 hover:text-slate-900 transition-colors">
|
||||||
{t('nav_features')}
|
{t('nav_features')}
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href="/foerderantrag" className="text-slate-600 hover:text-slate-900 transition-colors">
|
||||||
|
{t('nav_foerderantrag')}
|
||||||
|
</Link>
|
||||||
<Link href="/faq" className="text-slate-600 hover:text-slate-900 transition-colors">
|
<Link href="/faq" className="text-slate-600 hover:text-slate-900 transition-colors">
|
||||||
{t('nav_faq')}
|
{t('nav_faq')}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -77,6 +80,9 @@ export default function Header() {
|
|||||||
<Link href="/#features" className="text-slate-600 hover:text-slate-900">
|
<Link href="/#features" className="text-slate-600 hover:text-slate-900">
|
||||||
{t('nav_features')}
|
{t('nav_features')}
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href="/foerderantrag" className="text-slate-600 hover:text-slate-900">
|
||||||
|
{t('nav_foerderantrag')}
|
||||||
|
</Link>
|
||||||
<Link href="/faq" className="text-slate-600 hover:text-slate-900">
|
<Link href="/faq" className="text-slate-600 hover:text-slate-900">
|
||||||
{t('nav_faq')}
|
{t('nav_faq')}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useLanguage } from '@/lib/LanguageContext'
|
import { useLanguage } from '@/lib/LanguageContext'
|
||||||
import { WebsiteContent } from '@/lib/content-types'
|
import { WebsiteContent } from '@/lib/content-types'
|
||||||
|
import Link from 'next/link'
|
||||||
import PricingSection from './PricingSection'
|
import PricingSection from './PricingSection'
|
||||||
|
|
||||||
interface LandingContentProps {
|
interface LandingContentProps {
|
||||||
@@ -240,6 +241,67 @@ export default function LandingContent({ content }: LandingContentProps) {
|
|||||||
{/* Pricing Section */}
|
{/* Pricing Section */}
|
||||||
<PricingSection />
|
<PricingSection />
|
||||||
|
|
||||||
|
{/* Foerderantrag Section */}
|
||||||
|
<section className="py-20 px-4 sm:px-6 lg:px-8 bg-gradient-to-br from-blue-600 to-indigo-700 text-white">
|
||||||
|
<div className={`max-w-6xl mx-auto ${isRTL ? 'rtl' : ''}`}>
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h2 className="text-3xl sm:text-4xl font-bold">
|
||||||
|
{t('fa_section_title')}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 text-lg text-blue-100 max-w-3xl mx-auto">
|
||||||
|
{t('fa_section_subtitle')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
|
||||||
|
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 border border-white/20 hover:bg-white/15 transition-all">
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-white/20 flex items-center justify-center mb-4">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-lg mb-1">{t('fa_preset_basic_name')}</h3>
|
||||||
|
<p className="text-blue-100 text-sm mb-3">{t('fa_preset_basic_desc')}</p>
|
||||||
|
<p className="text-sm font-medium text-blue-200">{t('fa_preset_basic_budget')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 border border-white/20 hover:bg-white/15 transition-all">
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-white/20 flex items-center justify-center mb-4">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-lg mb-1">{t('fa_preset_cluster_name')}</h3>
|
||||||
|
<p className="text-blue-100 text-sm mb-3">{t('fa_preset_cluster_desc')}</p>
|
||||||
|
<p className="text-sm font-medium text-blue-200">{t('fa_preset_cluster_budget')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 border border-white/20 hover:bg-white/15 transition-all">
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-white/20 flex items-center justify-center mb-4">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-lg mb-1">{t('fa_preset_custom_name')}</h3>
|
||||||
|
<p className="text-blue-100 text-sm mb-3">{t('fa_preset_custom_desc')}</p>
|
||||||
|
<p className="text-sm font-medium text-blue-200">{t('fa_preset_custom_budget')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<Link
|
||||||
|
href="/foerderantrag"
|
||||||
|
className={`inline-flex items-center justify-center px-8 py-4 rounded-xl bg-white text-blue-700 font-semibold hover:bg-blue-50 transition-all shadow-lg ${isRTL ? 'flex-row-reverse' : ''}`}
|
||||||
|
>
|
||||||
|
{t('fa_section_cta')}
|
||||||
|
<svg className={`w-5 h-5 ${isRTL ? 'mr-2 rotate-180' : 'ml-2'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Trust Section */}
|
{/* Trust Section */}
|
||||||
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-slate-50">
|
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-slate-50">
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
|
|||||||
@@ -118,6 +118,89 @@ export const translations: Record<Language, Record<string, string>> = {
|
|||||||
admin_add_faq: '+ Frage hinzufuegen',
|
admin_add_faq: '+ Frage hinzufuegen',
|
||||||
admin_question: 'Frage',
|
admin_question: 'Frage',
|
||||||
admin_answer: 'Antwort (jede Zeile = neuer Absatz)',
|
admin_answer: 'Antwort (jede Zeile = neuer Absatz)',
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
nav_foerderantrag: 'Foerderantrag',
|
||||||
|
|
||||||
|
// Foerderantrag Landing Section
|
||||||
|
fa_section_title: 'Foerdergelder fuer Ihre Schule',
|
||||||
|
fa_section_subtitle: 'Erstellen Sie vollstaendige Foerderantraege fuer DigitalPakt 2.0 und Landesfoerderungen — mit KI-Unterstuetzung in 8 einfachen Schritten.',
|
||||||
|
fa_section_cta: 'Foerderantrag starten',
|
||||||
|
|
||||||
|
// Foerderantrag Overview
|
||||||
|
fa_title: 'Foerderantrag-Wizard',
|
||||||
|
fa_subtitle: 'Erstellen Sie vollstaendige Foerderantraege fuer DigitalPakt 2.0 und Landesfoerderungen. Der Wizard fuehrt Sie durch alle 8 Schritte und generiert antragsfaehige Dokumente.',
|
||||||
|
fa_new_application: 'Neuen Antrag starten',
|
||||||
|
fa_statistics: 'Statistik',
|
||||||
|
fa_preset_basic_name: 'BreakPilot Basis',
|
||||||
|
fa_preset_basic_desc: 'Lokale KI-Arbeitsstation fuer eine Schule. Vorausgefuellte Kostenplanung und Datenschutzkonzept.',
|
||||||
|
fa_preset_basic_budget: '~18.500 EUR Foerdervolumen',
|
||||||
|
fa_preset_cluster_name: 'BreakPilot Schulverbund',
|
||||||
|
fa_preset_cluster_desc: 'Zentrale KI-Infrastruktur fuer mehrere Schulen eines Traegers.',
|
||||||
|
fa_preset_cluster_budget: '~68.500 EUR Foerdervolumen',
|
||||||
|
fa_preset_custom_name: 'Individueller Antrag',
|
||||||
|
fa_preset_custom_desc: 'Leerer Wizard fuer individuelle Projekte. Volle Flexibilitaet bei der Planung.',
|
||||||
|
fa_preset_custom_budget: 'Beliebiges Foerdervolumen',
|
||||||
|
fa_no_applications: 'Noch keine Antraege',
|
||||||
|
fa_start_first: 'Starten Sie jetzt Ihren ersten Foerderantrag mit dem Wizard.',
|
||||||
|
fa_info_title: 'Wichtiger Hinweis',
|
||||||
|
fa_info_text: 'Der Wizard erstellt einen antragsfaehigen Entwurf. Die finale Pruefung und Einreichung erfolgt durch den Schultraeger. Alle generierten Dokumente koennen als ZIP heruntergeladen werden.',
|
||||||
|
|
||||||
|
// Foerderantrag New
|
||||||
|
fa_new_title: 'Neuen Foerderantrag starten',
|
||||||
|
fa_new_subtitle: 'Waehlen Sie das Foerderprogramm und Ihr Bundesland. Der Wizard fuehrt Sie durch alle weiteren Schritte.',
|
||||||
|
fa_back_overview: 'Zurueck zur Uebersicht',
|
||||||
|
fa_preset_label: 'Schnellstart mit Preset (optional)',
|
||||||
|
fa_program_label: 'Foerderprogramm',
|
||||||
|
fa_state_label: 'Bundesland',
|
||||||
|
fa_project_title_label: 'Projekttitel',
|
||||||
|
fa_ni_hint: 'Niedersachsen ist der Pilot-Standort mit optimaler Unterstuetzung.',
|
||||||
|
fa_submit: 'Wizard starten',
|
||||||
|
fa_cancel: 'Abbrechen',
|
||||||
|
fa_ai_hint_title: 'KI-Assistent verfuegbar',
|
||||||
|
fa_ai_hint_text: 'Im Wizard steht Ihnen ein KI-Assistent zur Seite, der bei Fragen hilft, Formulierungen vorschlaegt und Sie durch den Antragsprozess fuehrt.',
|
||||||
|
|
||||||
|
// Wizard Steps
|
||||||
|
fa_step1_title: 'Foerderprogramm',
|
||||||
|
fa_step1_subtitle: 'Programm & Grunddaten',
|
||||||
|
fa_step1_desc: 'Waehlen Sie das Foerderprogramm',
|
||||||
|
fa_step2_title: 'Schulinformationen',
|
||||||
|
fa_step2_subtitle: 'Schule & Traeger',
|
||||||
|
fa_step2_desc: 'Angaben zur Schule',
|
||||||
|
fa_step3_title: 'IT-Bestand',
|
||||||
|
fa_step3_subtitle: 'Aktuelle Infrastruktur',
|
||||||
|
fa_step3_desc: 'IT-Bestandsaufnahme',
|
||||||
|
fa_step4_title: 'Projektbeschreibung',
|
||||||
|
fa_step4_subtitle: 'Ziele & Didaktik',
|
||||||
|
fa_step4_desc: 'Projektziele beschreiben',
|
||||||
|
fa_step5_title: 'Investitionen',
|
||||||
|
fa_step5_subtitle: 'Kostenaufstellung',
|
||||||
|
fa_step5_desc: 'Geplante Anschaffungen',
|
||||||
|
fa_step6_title: 'Finanzierung',
|
||||||
|
fa_step6_subtitle: 'Budget & Eigenanteil',
|
||||||
|
fa_step6_desc: 'Finanzierungsplan',
|
||||||
|
fa_step7_title: 'Zeitplan',
|
||||||
|
fa_step7_subtitle: 'Laufzeit & Meilensteine',
|
||||||
|
fa_step7_desc: 'Projektlaufzeit planen',
|
||||||
|
fa_step8_title: 'Abschluss',
|
||||||
|
fa_step8_subtitle: 'Dokumente & Pruefung',
|
||||||
|
fa_step8_desc: 'Zusammenfassung',
|
||||||
|
fa_wizard_header: 'Foerderantrag bearbeiten',
|
||||||
|
fa_wizard_step_of: 'von',
|
||||||
|
fa_wizard_save: 'Speichern',
|
||||||
|
fa_wizard_saving: 'Speichern...',
|
||||||
|
fa_wizard_next: 'Weiter',
|
||||||
|
fa_wizard_prev: 'Zurueck',
|
||||||
|
fa_wizard_finish: 'Abschliessen',
|
||||||
|
fa_demo_mode: 'Demo-Modus',
|
||||||
|
fa_assistant_title: 'KI-Assistent',
|
||||||
|
fa_assistant_placeholder: 'Frage stellen...',
|
||||||
|
|
||||||
|
// Funding Programs
|
||||||
|
fa_program_dp2: 'DigitalPakt 2.0',
|
||||||
|
fa_program_dp1: 'DigitalPakt 1.0 (Restmittel)',
|
||||||
|
fa_program_landes: 'Landesfoerderung',
|
||||||
|
fa_program_traeger: 'Schultraegerfoerderung',
|
||||||
},
|
},
|
||||||
|
|
||||||
en: {
|
en: {
|
||||||
@@ -200,6 +283,83 @@ export const translations: Record<Language, Record<string, string>> = {
|
|||||||
admin_add_faq: '+ Add question',
|
admin_add_faq: '+ Add question',
|
||||||
admin_question: 'Question',
|
admin_question: 'Question',
|
||||||
admin_answer: 'Answer (each line = new paragraph)',
|
admin_answer: 'Answer (each line = new paragraph)',
|
||||||
|
|
||||||
|
nav_foerderantrag: 'Funding Application',
|
||||||
|
|
||||||
|
fa_section_title: 'Funding for Your School',
|
||||||
|
fa_section_subtitle: 'Create complete funding applications for DigitalPakt 2.0 and state funding programs — with AI support in 8 simple steps.',
|
||||||
|
fa_section_cta: 'Start Funding Application',
|
||||||
|
|
||||||
|
fa_title: 'Funding Application Wizard',
|
||||||
|
fa_subtitle: 'Create complete funding applications for DigitalPakt 2.0 and state funding programs. The wizard guides you through all 8 steps and generates application-ready documents.',
|
||||||
|
fa_new_application: 'Start New Application',
|
||||||
|
fa_statistics: 'Statistics',
|
||||||
|
fa_preset_basic_name: 'BreakPilot Basic',
|
||||||
|
fa_preset_basic_desc: 'Local AI workstation for one school. Pre-filled cost planning and data protection concept.',
|
||||||
|
fa_preset_basic_budget: '~18,500 EUR funding volume',
|
||||||
|
fa_preset_cluster_name: 'BreakPilot School Cluster',
|
||||||
|
fa_preset_cluster_desc: 'Central AI infrastructure for multiple schools of a provider.',
|
||||||
|
fa_preset_cluster_budget: '~68,500 EUR funding volume',
|
||||||
|
fa_preset_custom_name: 'Custom Application',
|
||||||
|
fa_preset_custom_desc: 'Empty wizard for individual projects. Full flexibility in planning.',
|
||||||
|
fa_preset_custom_budget: 'Any funding volume',
|
||||||
|
fa_no_applications: 'No applications yet',
|
||||||
|
fa_start_first: 'Start your first funding application with the wizard now.',
|
||||||
|
fa_info_title: 'Important Note',
|
||||||
|
fa_info_text: 'The wizard creates an application-ready draft. Final review and submission is done by the school authority. All generated documents can be downloaded as ZIP.',
|
||||||
|
|
||||||
|
fa_new_title: 'Start New Funding Application',
|
||||||
|
fa_new_subtitle: 'Select the funding program and your federal state. The wizard guides you through all further steps.',
|
||||||
|
fa_back_overview: 'Back to Overview',
|
||||||
|
fa_preset_label: 'Quick start with preset (optional)',
|
||||||
|
fa_program_label: 'Funding Program',
|
||||||
|
fa_state_label: 'Federal State',
|
||||||
|
fa_project_title_label: 'Project Title',
|
||||||
|
fa_ni_hint: 'Lower Saxony is the pilot location with optimal support.',
|
||||||
|
fa_submit: 'Start Wizard',
|
||||||
|
fa_cancel: 'Cancel',
|
||||||
|
fa_ai_hint_title: 'AI Assistant Available',
|
||||||
|
fa_ai_hint_text: 'The wizard includes an AI assistant that helps with questions, suggests wordings, and guides you through the application process.',
|
||||||
|
|
||||||
|
fa_step1_title: 'Funding Program',
|
||||||
|
fa_step1_subtitle: 'Program & Basic Data',
|
||||||
|
fa_step1_desc: 'Select the funding program',
|
||||||
|
fa_step2_title: 'School Information',
|
||||||
|
fa_step2_subtitle: 'School & Authority',
|
||||||
|
fa_step2_desc: 'School details',
|
||||||
|
fa_step3_title: 'IT Inventory',
|
||||||
|
fa_step3_subtitle: 'Current Infrastructure',
|
||||||
|
fa_step3_desc: 'IT inventory assessment',
|
||||||
|
fa_step4_title: 'Project Description',
|
||||||
|
fa_step4_subtitle: 'Goals & Didactics',
|
||||||
|
fa_step4_desc: 'Describe project goals',
|
||||||
|
fa_step5_title: 'Investments',
|
||||||
|
fa_step5_subtitle: 'Cost Breakdown',
|
||||||
|
fa_step5_desc: 'Planned acquisitions',
|
||||||
|
fa_step6_title: 'Financing',
|
||||||
|
fa_step6_subtitle: 'Budget & Co-funding',
|
||||||
|
fa_step6_desc: 'Financing plan',
|
||||||
|
fa_step7_title: 'Timeline',
|
||||||
|
fa_step7_subtitle: 'Duration & Milestones',
|
||||||
|
fa_step7_desc: 'Plan project timeline',
|
||||||
|
fa_step8_title: 'Completion',
|
||||||
|
fa_step8_subtitle: 'Documents & Review',
|
||||||
|
fa_step8_desc: 'Summary',
|
||||||
|
fa_wizard_header: 'Edit Funding Application',
|
||||||
|
fa_wizard_step_of: 'of',
|
||||||
|
fa_wizard_save: 'Save',
|
||||||
|
fa_wizard_saving: 'Saving...',
|
||||||
|
fa_wizard_next: 'Next',
|
||||||
|
fa_wizard_prev: 'Back',
|
||||||
|
fa_wizard_finish: 'Complete',
|
||||||
|
fa_demo_mode: 'Demo Mode',
|
||||||
|
fa_assistant_title: 'AI Assistant',
|
||||||
|
fa_assistant_placeholder: 'Ask a question...',
|
||||||
|
|
||||||
|
fa_program_dp2: 'DigitalPakt 2.0',
|
||||||
|
fa_program_dp1: 'DigitalPakt 1.0 (Remaining)',
|
||||||
|
fa_program_landes: 'State Funding',
|
||||||
|
fa_program_traeger: 'School Authority Funding',
|
||||||
},
|
},
|
||||||
|
|
||||||
tr: {
|
tr: {
|
||||||
@@ -282,6 +442,83 @@ export const translations: Record<Language, Record<string, string>> = {
|
|||||||
admin_add_faq: '+ Soru ekle',
|
admin_add_faq: '+ Soru ekle',
|
||||||
admin_question: 'Soru',
|
admin_question: 'Soru',
|
||||||
admin_answer: 'Cevap (her satir = yeni paragraf)',
|
admin_answer: 'Cevap (her satir = yeni paragraf)',
|
||||||
|
|
||||||
|
nav_foerderantrag: 'Hibe Basvurusu',
|
||||||
|
|
||||||
|
fa_section_title: 'Okulunuz icin Hibe',
|
||||||
|
fa_section_subtitle: 'DigitalPakt 2.0 ve eyalet hibeleri icin eksiksiz basvurular olusturun — 8 basit adimda yapay zeka destegi ile.',
|
||||||
|
fa_section_cta: 'Hibe Basvurusu Basla',
|
||||||
|
|
||||||
|
fa_title: 'Hibe Basvuru Sihirbazi',
|
||||||
|
fa_subtitle: 'DigitalPakt 2.0 ve eyalet hibeleri icin eksiksiz basvurular olusturun. Sihirbaz sizi 8 adimda yonlendirir.',
|
||||||
|
fa_new_application: 'Yeni Basvuru Basla',
|
||||||
|
fa_statistics: 'Istatistikler',
|
||||||
|
fa_preset_basic_name: 'BreakPilot Temel',
|
||||||
|
fa_preset_basic_desc: 'Bir okul icin yerel yapay zeka calisma istasyonu.',
|
||||||
|
fa_preset_basic_budget: '~18.500 EUR hibe tutari',
|
||||||
|
fa_preset_cluster_name: 'BreakPilot Okul Birligi',
|
||||||
|
fa_preset_cluster_desc: 'Birden fazla okul icin merkezi yapay zeka altyapisi.',
|
||||||
|
fa_preset_cluster_budget: '~68.500 EUR hibe tutari',
|
||||||
|
fa_preset_custom_name: 'Bireysel Basvuru',
|
||||||
|
fa_preset_custom_desc: 'Bireysel projeler icin bos sihirbaz.',
|
||||||
|
fa_preset_custom_budget: 'Herhangi bir hibe tutari',
|
||||||
|
fa_no_applications: 'Henuz basvuru yok',
|
||||||
|
fa_start_first: 'Simdi sihirbaz ile ilk hibe basvurunuzu baslatin.',
|
||||||
|
fa_info_title: 'Onemli Not',
|
||||||
|
fa_info_text: 'Sihirbaz basvuruya hazir bir taslak olusturur. Son inceleme ve gonderim okul yetkilisi tarafindan yapilir.',
|
||||||
|
|
||||||
|
fa_new_title: 'Yeni Hibe Basvurusu Basla',
|
||||||
|
fa_new_subtitle: 'Hibe programini ve eyaletinizi secin. Sihirbaz diger adimlarda size rehberlik eder.',
|
||||||
|
fa_back_overview: 'Genel Bakisa Don',
|
||||||
|
fa_preset_label: 'Onayarli hizli baslangic (istege bagli)',
|
||||||
|
fa_program_label: 'Hibe Programi',
|
||||||
|
fa_state_label: 'Eyalet',
|
||||||
|
fa_project_title_label: 'Proje Basligi',
|
||||||
|
fa_ni_hint: 'Asagi Saksonya optimal destekle pilot lokasyondur.',
|
||||||
|
fa_submit: 'Sihirbazi Basla',
|
||||||
|
fa_cancel: 'Iptal',
|
||||||
|
fa_ai_hint_title: 'Yapay Zeka Asistani Mevcut',
|
||||||
|
fa_ai_hint_text: 'Sihirbazda size yardimci olan bir yapay zeka asistani bulunmaktadir.',
|
||||||
|
|
||||||
|
fa_step1_title: 'Hibe Programi',
|
||||||
|
fa_step1_subtitle: 'Program ve Temel Veriler',
|
||||||
|
fa_step1_desc: 'Hibe programini secin',
|
||||||
|
fa_step2_title: 'Okul Bilgileri',
|
||||||
|
fa_step2_subtitle: 'Okul ve Yetkili',
|
||||||
|
fa_step2_desc: 'Okul detaylari',
|
||||||
|
fa_step3_title: 'BT Envanteri',
|
||||||
|
fa_step3_subtitle: 'Mevcut Altyapi',
|
||||||
|
fa_step3_desc: 'BT envanter degerlendirmesi',
|
||||||
|
fa_step4_title: 'Proje Aciklamasi',
|
||||||
|
fa_step4_subtitle: 'Hedefler ve Didaktik',
|
||||||
|
fa_step4_desc: 'Proje hedeflerini tanimlayin',
|
||||||
|
fa_step5_title: 'Yatirimlar',
|
||||||
|
fa_step5_subtitle: 'Maliyet Dokumu',
|
||||||
|
fa_step5_desc: 'Planlanan satin almalar',
|
||||||
|
fa_step6_title: 'Finansman',
|
||||||
|
fa_step6_subtitle: 'Butce ve Es-finansman',
|
||||||
|
fa_step6_desc: 'Finansman plani',
|
||||||
|
fa_step7_title: 'Zaman Cizelgesi',
|
||||||
|
fa_step7_subtitle: 'Sure ve Kilometre Taslari',
|
||||||
|
fa_step7_desc: 'Proje zaman cizelgesini planlayin',
|
||||||
|
fa_step8_title: 'Tamamlama',
|
||||||
|
fa_step8_subtitle: 'Belgeler ve Inceleme',
|
||||||
|
fa_step8_desc: 'Ozet',
|
||||||
|
fa_wizard_header: 'Hibe Basvurusunu Duzenle',
|
||||||
|
fa_wizard_step_of: '/',
|
||||||
|
fa_wizard_save: 'Kaydet',
|
||||||
|
fa_wizard_saving: 'Kaydediliyor...',
|
||||||
|
fa_wizard_next: 'Ileri',
|
||||||
|
fa_wizard_prev: 'Geri',
|
||||||
|
fa_wizard_finish: 'Tamamla',
|
||||||
|
fa_demo_mode: 'Demo Modu',
|
||||||
|
fa_assistant_title: 'Yapay Zeka Asistani',
|
||||||
|
fa_assistant_placeholder: 'Soru sorun...',
|
||||||
|
|
||||||
|
fa_program_dp2: 'DigitalPakt 2.0',
|
||||||
|
fa_program_dp1: 'DigitalPakt 1.0 (Kalan)',
|
||||||
|
fa_program_landes: 'Eyalet Hibesi',
|
||||||
|
fa_program_traeger: 'Okul Yetkilisi Hibesi',
|
||||||
},
|
},
|
||||||
|
|
||||||
ar: {
|
ar: {
|
||||||
@@ -364,6 +601,83 @@ export const translations: Record<Language, Record<string, string>> = {
|
|||||||
admin_add_faq: '+ اضافة سؤال',
|
admin_add_faq: '+ اضافة سؤال',
|
||||||
admin_question: 'السؤال',
|
admin_question: 'السؤال',
|
||||||
admin_answer: 'الاجابة (كل سطر = فقرة جديدة)',
|
admin_answer: 'الاجابة (كل سطر = فقرة جديدة)',
|
||||||
|
|
||||||
|
nav_foerderantrag: 'طلب التمويل',
|
||||||
|
|
||||||
|
fa_section_title: 'تمويل لمدرستك',
|
||||||
|
fa_section_subtitle: 'انشئ طلبات تمويل كاملة لـ DigitalPakt 2.0 وبرامج التمويل الحكومية — بدعم الذكاء الاصطناعي في 8 خطوات بسيطة.',
|
||||||
|
fa_section_cta: 'ابدا طلب التمويل',
|
||||||
|
|
||||||
|
fa_title: 'معالج طلب التمويل',
|
||||||
|
fa_subtitle: 'انشئ طلبات تمويل كاملة لـ DigitalPakt 2.0 وبرامج التمويل الحكومية. يرشدك المعالج خلال 8 خطوات.',
|
||||||
|
fa_new_application: 'بدء طلب جديد',
|
||||||
|
fa_statistics: 'الاحصائيات',
|
||||||
|
fa_preset_basic_name: 'BreakPilot الاساسي',
|
||||||
|
fa_preset_basic_desc: 'محطة عمل ذكاء اصطناعي محلية لمدرسة واحدة.',
|
||||||
|
fa_preset_basic_budget: '~18,500 يورو حجم التمويل',
|
||||||
|
fa_preset_cluster_name: 'BreakPilot مجموعة المدارس',
|
||||||
|
fa_preset_cluster_desc: 'بنية تحتية مركزية للذكاء الاصطناعي لعدة مدارس.',
|
||||||
|
fa_preset_cluster_budget: '~68,500 يورو حجم التمويل',
|
||||||
|
fa_preset_custom_name: 'طلب فردي',
|
||||||
|
fa_preset_custom_desc: 'معالج فارغ للمشاريع الفردية.',
|
||||||
|
fa_preset_custom_budget: 'اي حجم تمويل',
|
||||||
|
fa_no_applications: 'لا توجد طلبات بعد',
|
||||||
|
fa_start_first: 'ابدا طلب التمويل الاول مع المعالج الان.',
|
||||||
|
fa_info_title: 'ملاحظة مهمة',
|
||||||
|
fa_info_text: 'ينشئ المعالج مسودة جاهزة للتقديم. المراجعة والتقديم النهائي تتم من قبل الجهة المسؤولة.',
|
||||||
|
|
||||||
|
fa_new_title: 'بدء طلب تمويل جديد',
|
||||||
|
fa_new_subtitle: 'اختر برنامج التمويل وولايتك. يرشدك المعالج خلال باقي الخطوات.',
|
||||||
|
fa_back_overview: 'العودة للنظرة العامة',
|
||||||
|
fa_preset_label: 'بداية سريعة مع اعداد مسبق (اختياري)',
|
||||||
|
fa_program_label: 'برنامج التمويل',
|
||||||
|
fa_state_label: 'الولاية',
|
||||||
|
fa_project_title_label: 'عنوان المشروع',
|
||||||
|
fa_ni_hint: 'ساكسونيا السفلى هي الموقع التجريبي مع الدعم الامثل.',
|
||||||
|
fa_submit: 'بدء المعالج',
|
||||||
|
fa_cancel: 'الغاء',
|
||||||
|
fa_ai_hint_title: 'مساعد الذكاء الاصطناعي متاح',
|
||||||
|
fa_ai_hint_text: 'يتضمن المعالج مساعد ذكاء اصطناعي يساعدك في الاسئلة ويقترح الصياغات.',
|
||||||
|
|
||||||
|
fa_step1_title: 'برنامج التمويل',
|
||||||
|
fa_step1_subtitle: 'البرنامج والبيانات الاساسية',
|
||||||
|
fa_step1_desc: 'اختر برنامج التمويل',
|
||||||
|
fa_step2_title: 'معلومات المدرسة',
|
||||||
|
fa_step2_subtitle: 'المدرسة والجهة',
|
||||||
|
fa_step2_desc: 'تفاصيل المدرسة',
|
||||||
|
fa_step3_title: 'جرد تقنية المعلومات',
|
||||||
|
fa_step3_subtitle: 'البنية التحتية الحالية',
|
||||||
|
fa_step3_desc: 'تقييم جرد تقنية المعلومات',
|
||||||
|
fa_step4_title: 'وصف المشروع',
|
||||||
|
fa_step4_subtitle: 'الاهداف والتعليم',
|
||||||
|
fa_step4_desc: 'وصف اهداف المشروع',
|
||||||
|
fa_step5_title: 'الاستثمارات',
|
||||||
|
fa_step5_subtitle: 'تفصيل التكاليف',
|
||||||
|
fa_step5_desc: 'المشتريات المخططة',
|
||||||
|
fa_step6_title: 'التمويل',
|
||||||
|
fa_step6_subtitle: 'الميزانية والتمويل المشترك',
|
||||||
|
fa_step6_desc: 'خطة التمويل',
|
||||||
|
fa_step7_title: 'الجدول الزمني',
|
||||||
|
fa_step7_subtitle: 'المدة والمراحل',
|
||||||
|
fa_step7_desc: 'تخطيط الجدول الزمني',
|
||||||
|
fa_step8_title: 'الاكمال',
|
||||||
|
fa_step8_subtitle: 'المستندات والمراجعة',
|
||||||
|
fa_step8_desc: 'الملخص',
|
||||||
|
fa_wizard_header: 'تعديل طلب التمويل',
|
||||||
|
fa_wizard_step_of: 'من',
|
||||||
|
fa_wizard_save: 'حفظ',
|
||||||
|
fa_wizard_saving: 'جاري الحفظ...',
|
||||||
|
fa_wizard_next: 'التالي',
|
||||||
|
fa_wizard_prev: 'السابق',
|
||||||
|
fa_wizard_finish: 'اكمال',
|
||||||
|
fa_demo_mode: 'وضع تجريبي',
|
||||||
|
fa_assistant_title: 'مساعد الذكاء الاصطناعي',
|
||||||
|
fa_assistant_placeholder: 'اطرح سؤالا...',
|
||||||
|
|
||||||
|
fa_program_dp2: 'DigitalPakt 2.0',
|
||||||
|
fa_program_dp1: 'DigitalPakt 1.0 (المتبقي)',
|
||||||
|
fa_program_landes: 'تمويل حكومي',
|
||||||
|
fa_program_traeger: 'تمويل الجهة المسؤولة',
|
||||||
},
|
},
|
||||||
|
|
||||||
ru: {
|
ru: {
|
||||||
@@ -446,6 +760,83 @@ export const translations: Record<Language, Record<string, string>> = {
|
|||||||
admin_add_faq: '+ Добавить вопрос',
|
admin_add_faq: '+ Добавить вопрос',
|
||||||
admin_question: 'Вопрос',
|
admin_question: 'Вопрос',
|
||||||
admin_answer: 'Ответ (каждая строка = новый абзац)',
|
admin_answer: 'Ответ (каждая строка = новый абзац)',
|
||||||
|
|
||||||
|
nav_foerderantrag: 'Заявка на грант',
|
||||||
|
|
||||||
|
fa_section_title: 'Финансирование для вашей школы',
|
||||||
|
fa_section_subtitle: 'Создавайте полные заявки на гранты для DigitalPakt 2.0 и государственных программ — с поддержкой ИИ за 8 простых шагов.',
|
||||||
|
fa_section_cta: 'Начать заявку на грант',
|
||||||
|
|
||||||
|
fa_title: 'Мастер заявки на грант',
|
||||||
|
fa_subtitle: 'Создавайте полные заявки на гранты для DigitalPakt 2.0 и государственных программ. Мастер проведет вас через все 8 шагов.',
|
||||||
|
fa_new_application: 'Начать новую заявку',
|
||||||
|
fa_statistics: 'Статистика',
|
||||||
|
fa_preset_basic_name: 'BreakPilot Базовый',
|
||||||
|
fa_preset_basic_desc: 'Локальная ИИ-станция для одной школы.',
|
||||||
|
fa_preset_basic_budget: '~18 500 EUR объем гранта',
|
||||||
|
fa_preset_cluster_name: 'BreakPilot Школьный кластер',
|
||||||
|
fa_preset_cluster_desc: 'Центральная ИИ-инфраструктура для нескольких школ.',
|
||||||
|
fa_preset_cluster_budget: '~68 500 EUR объем гранта',
|
||||||
|
fa_preset_custom_name: 'Индивидуальная заявка',
|
||||||
|
fa_preset_custom_desc: 'Пустой мастер для индивидуальных проектов.',
|
||||||
|
fa_preset_custom_budget: 'Любой объем гранта',
|
||||||
|
fa_no_applications: 'Заявок пока нет',
|
||||||
|
fa_start_first: 'Начните свою первую заявку на грант с помощью мастера.',
|
||||||
|
fa_info_title: 'Важное примечание',
|
||||||
|
fa_info_text: 'Мастер создает готовый к подаче черновик. Окончательная проверка и подача осуществляется учредителем школы.',
|
||||||
|
|
||||||
|
fa_new_title: 'Начать новую заявку на грант',
|
||||||
|
fa_new_subtitle: 'Выберите программу финансирования и вашу федеральную землю. Мастер проведет вас через все шаги.',
|
||||||
|
fa_back_overview: 'Назад к обзору',
|
||||||
|
fa_preset_label: 'Быстрый старт с шаблоном (необязательно)',
|
||||||
|
fa_program_label: 'Программа финансирования',
|
||||||
|
fa_state_label: 'Федеральная земля',
|
||||||
|
fa_project_title_label: 'Название проекта',
|
||||||
|
fa_ni_hint: 'Нижняя Саксония — пилотный регион с оптимальной поддержкой.',
|
||||||
|
fa_submit: 'Запустить мастер',
|
||||||
|
fa_cancel: 'Отмена',
|
||||||
|
fa_ai_hint_title: 'ИИ-ассистент доступен',
|
||||||
|
fa_ai_hint_text: 'В мастере есть ИИ-ассистент, который помогает с вопросами и предлагает формулировки.',
|
||||||
|
|
||||||
|
fa_step1_title: 'Программа',
|
||||||
|
fa_step1_subtitle: 'Программа и основные данные',
|
||||||
|
fa_step1_desc: 'Выберите программу финансирования',
|
||||||
|
fa_step2_title: 'Информация о школе',
|
||||||
|
fa_step2_subtitle: 'Школа и учредитель',
|
||||||
|
fa_step2_desc: 'Данные школы',
|
||||||
|
fa_step3_title: 'ИТ-инвентаризация',
|
||||||
|
fa_step3_subtitle: 'Текущая инфраструктура',
|
||||||
|
fa_step3_desc: 'Оценка ИТ-инвентаря',
|
||||||
|
fa_step4_title: 'Описание проекта',
|
||||||
|
fa_step4_subtitle: 'Цели и дидактика',
|
||||||
|
fa_step4_desc: 'Описание целей проекта',
|
||||||
|
fa_step5_title: 'Инвестиции',
|
||||||
|
fa_step5_subtitle: 'Смета расходов',
|
||||||
|
fa_step5_desc: 'Планируемые закупки',
|
||||||
|
fa_step6_title: 'Финансирование',
|
||||||
|
fa_step6_subtitle: 'Бюджет и софинансирование',
|
||||||
|
fa_step6_desc: 'План финансирования',
|
||||||
|
fa_step7_title: 'График',
|
||||||
|
fa_step7_subtitle: 'Сроки и этапы',
|
||||||
|
fa_step7_desc: 'Планирование сроков',
|
||||||
|
fa_step8_title: 'Завершение',
|
||||||
|
fa_step8_subtitle: 'Документы и проверка',
|
||||||
|
fa_step8_desc: 'Итоги',
|
||||||
|
fa_wizard_header: 'Редактирование заявки',
|
||||||
|
fa_wizard_step_of: 'из',
|
||||||
|
fa_wizard_save: 'Сохранить',
|
||||||
|
fa_wizard_saving: 'Сохранение...',
|
||||||
|
fa_wizard_next: 'Далее',
|
||||||
|
fa_wizard_prev: 'Назад',
|
||||||
|
fa_wizard_finish: 'Завершить',
|
||||||
|
fa_demo_mode: 'Демо-режим',
|
||||||
|
fa_assistant_title: 'ИИ-ассистент',
|
||||||
|
fa_assistant_placeholder: 'Задать вопрос...',
|
||||||
|
|
||||||
|
fa_program_dp2: 'DigitalPakt 2.0',
|
||||||
|
fa_program_dp1: 'DigitalPakt 1.0 (остаток)',
|
||||||
|
fa_program_landes: 'Госфинансирование',
|
||||||
|
fa_program_traeger: 'Финансирование учредителя',
|
||||||
},
|
},
|
||||||
|
|
||||||
uk: {
|
uk: {
|
||||||
@@ -528,6 +919,83 @@ export const translations: Record<Language, Record<string, string>> = {
|
|||||||
admin_add_faq: '+ Додати питання',
|
admin_add_faq: '+ Додати питання',
|
||||||
admin_question: 'Питання',
|
admin_question: 'Питання',
|
||||||
admin_answer: 'Відповідь (кожен рядок = новий абзац)',
|
admin_answer: 'Відповідь (кожен рядок = новий абзац)',
|
||||||
|
|
||||||
|
nav_foerderantrag: 'Заявка на грант',
|
||||||
|
|
||||||
|
fa_section_title: 'Фінансування для вашої школи',
|
||||||
|
fa_section_subtitle: 'Створюйте повні заявки на гранти для DigitalPakt 2.0 та державних програм — з підтримкою ШІ за 8 простих кроків.',
|
||||||
|
fa_section_cta: 'Почати заявку на грант',
|
||||||
|
|
||||||
|
fa_title: 'Майстер заявки на грант',
|
||||||
|
fa_subtitle: 'Створюйте повні заявки на гранти для DigitalPakt 2.0 та державних програм. Майстер проведе вас через усі 8 кроків.',
|
||||||
|
fa_new_application: 'Почати нову заявку',
|
||||||
|
fa_statistics: 'Статистика',
|
||||||
|
fa_preset_basic_name: 'BreakPilot Базовий',
|
||||||
|
fa_preset_basic_desc: 'Локальна ШІ-станція для однієї школи.',
|
||||||
|
fa_preset_basic_budget: '~18 500 EUR обсяг гранту',
|
||||||
|
fa_preset_cluster_name: 'BreakPilot Шкільний кластер',
|
||||||
|
fa_preset_cluster_desc: 'Центральна ШІ-інфраструктура для кількох шкіл.',
|
||||||
|
fa_preset_cluster_budget: '~68 500 EUR обсяг гранту',
|
||||||
|
fa_preset_custom_name: 'Індивідуальна заявка',
|
||||||
|
fa_preset_custom_desc: 'Порожній майстер для індивідуальних проектів.',
|
||||||
|
fa_preset_custom_budget: 'Будь-який обсяг гранту',
|
||||||
|
fa_no_applications: 'Заявок поки немає',
|
||||||
|
fa_start_first: 'Почніть свою першу заявку на грант за допомогою майстра.',
|
||||||
|
fa_info_title: 'Важливе зауваження',
|
||||||
|
fa_info_text: 'Майстер створює готовий до подання чернетку. Остаточна перевірка та подання здійснюється засновником школи.',
|
||||||
|
|
||||||
|
fa_new_title: 'Почати нову заявку на грант',
|
||||||
|
fa_new_subtitle: 'Виберіть програму фінансування та вашу федеральну землю. Майстер проведе вас через усі кроки.',
|
||||||
|
fa_back_overview: 'Назад до огляду',
|
||||||
|
fa_preset_label: 'Швидкий старт з шаблоном (необов\'язково)',
|
||||||
|
fa_program_label: 'Програма фінансування',
|
||||||
|
fa_state_label: 'Федеральна земля',
|
||||||
|
fa_project_title_label: 'Назва проекту',
|
||||||
|
fa_ni_hint: 'Нижня Саксонія — пілотний регіон з оптимальною підтримкою.',
|
||||||
|
fa_submit: 'Запустити майстер',
|
||||||
|
fa_cancel: 'Скасувати',
|
||||||
|
fa_ai_hint_title: 'ШІ-асистент доступний',
|
||||||
|
fa_ai_hint_text: 'У майстрі є ШІ-асистент, який допомагає з питаннями та пропонує формулювання.',
|
||||||
|
|
||||||
|
fa_step1_title: 'Програма',
|
||||||
|
fa_step1_subtitle: 'Програма та основні дані',
|
||||||
|
fa_step1_desc: 'Виберіть програму фінансування',
|
||||||
|
fa_step2_title: 'Інформація про школу',
|
||||||
|
fa_step2_subtitle: 'Школа та засновник',
|
||||||
|
fa_step2_desc: 'Дані школи',
|
||||||
|
fa_step3_title: 'ІТ-інвентаризація',
|
||||||
|
fa_step3_subtitle: 'Поточна інфраструктура',
|
||||||
|
fa_step3_desc: 'Оцінка ІТ-інвентарю',
|
||||||
|
fa_step4_title: 'Опис проекту',
|
||||||
|
fa_step4_subtitle: 'Цілі та дидактика',
|
||||||
|
fa_step4_desc: 'Опис цілей проекту',
|
||||||
|
fa_step5_title: 'Інвестиції',
|
||||||
|
fa_step5_subtitle: 'Кошторис витрат',
|
||||||
|
fa_step5_desc: 'Заплановані закупівлі',
|
||||||
|
fa_step6_title: 'Фінансування',
|
||||||
|
fa_step6_subtitle: 'Бюджет та співфінансування',
|
||||||
|
fa_step6_desc: 'План фінансування',
|
||||||
|
fa_step7_title: 'Графік',
|
||||||
|
fa_step7_subtitle: 'Терміни та етапи',
|
||||||
|
fa_step7_desc: 'Планування термінів',
|
||||||
|
fa_step8_title: 'Завершення',
|
||||||
|
fa_step8_subtitle: 'Документи та перевірка',
|
||||||
|
fa_step8_desc: 'Підсумки',
|
||||||
|
fa_wizard_header: 'Редагування заявки',
|
||||||
|
fa_wizard_step_of: 'з',
|
||||||
|
fa_wizard_save: 'Зберегти',
|
||||||
|
fa_wizard_saving: 'Збереження...',
|
||||||
|
fa_wizard_next: 'Далі',
|
||||||
|
fa_wizard_prev: 'Назад',
|
||||||
|
fa_wizard_finish: 'Завершити',
|
||||||
|
fa_demo_mode: 'Демо-режим',
|
||||||
|
fa_assistant_title: 'ШІ-асистент',
|
||||||
|
fa_assistant_placeholder: 'Поставити питання...',
|
||||||
|
|
||||||
|
fa_program_dp2: 'DigitalPakt 2.0',
|
||||||
|
fa_program_dp1: 'DigitalPakt 1.0 (залишок)',
|
||||||
|
fa_program_landes: 'Держфінансування',
|
||||||
|
fa_program_traeger: 'Фінансування засновника',
|
||||||
},
|
},
|
||||||
|
|
||||||
pl: {
|
pl: {
|
||||||
@@ -610,6 +1078,83 @@ export const translations: Record<Language, Record<string, string>> = {
|
|||||||
admin_add_faq: '+ Dodaj pytanie',
|
admin_add_faq: '+ Dodaj pytanie',
|
||||||
admin_question: 'Pytanie',
|
admin_question: 'Pytanie',
|
||||||
admin_answer: 'Odpowiedź (każda linia = nowy akapit)',
|
admin_answer: 'Odpowiedź (każda linia = nowy akapit)',
|
||||||
|
|
||||||
|
nav_foerderantrag: 'Wniosek o dotację',
|
||||||
|
|
||||||
|
fa_section_title: 'Dotacje dla Twojej szkoły',
|
||||||
|
fa_section_subtitle: 'Twórz kompletne wnioski o dotacje dla DigitalPakt 2.0 i programów państwowych — ze wsparciem AI w 8 prostych krokach.',
|
||||||
|
fa_section_cta: 'Rozpocznij wniosek',
|
||||||
|
|
||||||
|
fa_title: 'Kreator wniosku o dotację',
|
||||||
|
fa_subtitle: 'Twórz kompletne wnioski o dotacje dla DigitalPakt 2.0 i programów państwowych. Kreator przeprowadzi Cię przez 8 kroków.',
|
||||||
|
fa_new_application: 'Nowy wniosek',
|
||||||
|
fa_statistics: 'Statystyki',
|
||||||
|
fa_preset_basic_name: 'BreakPilot Podstawowy',
|
||||||
|
fa_preset_basic_desc: 'Lokalna stacja robocza AI dla jednej szkoły.',
|
||||||
|
fa_preset_basic_budget: '~18 500 EUR kwota dotacji',
|
||||||
|
fa_preset_cluster_name: 'BreakPilot Klaster Szkół',
|
||||||
|
fa_preset_cluster_desc: 'Centralna infrastruktura AI dla wielu szkół.',
|
||||||
|
fa_preset_cluster_budget: '~68 500 EUR kwota dotacji',
|
||||||
|
fa_preset_custom_name: 'Wniosek indywidualny',
|
||||||
|
fa_preset_custom_desc: 'Pusty kreator dla indywidualnych projektów.',
|
||||||
|
fa_preset_custom_budget: 'Dowolna kwota dotacji',
|
||||||
|
fa_no_applications: 'Brak wniosków',
|
||||||
|
fa_start_first: 'Rozpocznij swój pierwszy wniosek o dotację za pomocą kreatora.',
|
||||||
|
fa_info_title: 'Ważna uwaga',
|
||||||
|
fa_info_text: 'Kreator tworzy gotowy do złożenia szkic. Ostateczna weryfikacja i złożenie odbywa się przez organ prowadzący szkołę.',
|
||||||
|
|
||||||
|
fa_new_title: 'Nowy wniosek o dotację',
|
||||||
|
fa_new_subtitle: 'Wybierz program dotacyjny i swój kraj związkowy. Kreator przeprowadzi Cię przez kolejne kroki.',
|
||||||
|
fa_back_overview: 'Powrót do przeglądu',
|
||||||
|
fa_preset_label: 'Szybki start z szablonem (opcjonalnie)',
|
||||||
|
fa_program_label: 'Program dotacyjny',
|
||||||
|
fa_state_label: 'Kraj związkowy',
|
||||||
|
fa_project_title_label: 'Tytuł projektu',
|
||||||
|
fa_ni_hint: 'Dolna Saksonia to lokalizacja pilotażowa z optymalnym wsparciem.',
|
||||||
|
fa_submit: 'Uruchom kreator',
|
||||||
|
fa_cancel: 'Anuluj',
|
||||||
|
fa_ai_hint_title: 'Asystent AI dostępny',
|
||||||
|
fa_ai_hint_text: 'Kreator zawiera asystenta AI, który pomaga przy pytaniach i sugeruje sformułowania.',
|
||||||
|
|
||||||
|
fa_step1_title: 'Program',
|
||||||
|
fa_step1_subtitle: 'Program i dane podstawowe',
|
||||||
|
fa_step1_desc: 'Wybierz program dotacyjny',
|
||||||
|
fa_step2_title: 'Informacje o szkole',
|
||||||
|
fa_step2_subtitle: 'Szkoła i organ prowadzący',
|
||||||
|
fa_step2_desc: 'Dane szkoły',
|
||||||
|
fa_step3_title: 'Inwentaryzacja IT',
|
||||||
|
fa_step3_subtitle: 'Obecna infrastruktura',
|
||||||
|
fa_step3_desc: 'Ocena inwentarza IT',
|
||||||
|
fa_step4_title: 'Opis projektu',
|
||||||
|
fa_step4_subtitle: 'Cele i dydaktyka',
|
||||||
|
fa_step4_desc: 'Opis celów projektu',
|
||||||
|
fa_step5_title: 'Inwestycje',
|
||||||
|
fa_step5_subtitle: 'Kosztorys',
|
||||||
|
fa_step5_desc: 'Planowane zakupy',
|
||||||
|
fa_step6_title: 'Finansowanie',
|
||||||
|
fa_step6_subtitle: 'Budżet i współfinansowanie',
|
||||||
|
fa_step6_desc: 'Plan finansowania',
|
||||||
|
fa_step7_title: 'Harmonogram',
|
||||||
|
fa_step7_subtitle: 'Czas trwania i kamienie milowe',
|
||||||
|
fa_step7_desc: 'Planowanie harmonogramu',
|
||||||
|
fa_step8_title: 'Zakończenie',
|
||||||
|
fa_step8_subtitle: 'Dokumenty i weryfikacja',
|
||||||
|
fa_step8_desc: 'Podsumowanie',
|
||||||
|
fa_wizard_header: 'Edycja wniosku',
|
||||||
|
fa_wizard_step_of: 'z',
|
||||||
|
fa_wizard_save: 'Zapisz',
|
||||||
|
fa_wizard_saving: 'Zapisywanie...',
|
||||||
|
fa_wizard_next: 'Dalej',
|
||||||
|
fa_wizard_prev: 'Wstecz',
|
||||||
|
fa_wizard_finish: 'Zakończ',
|
||||||
|
fa_demo_mode: 'Tryb demo',
|
||||||
|
fa_assistant_title: 'Asystent AI',
|
||||||
|
fa_assistant_placeholder: 'Zadaj pytanie...',
|
||||||
|
|
||||||
|
fa_program_dp2: 'DigitalPakt 2.0',
|
||||||
|
fa_program_dp1: 'DigitalPakt 1.0 (reszta)',
|
||||||
|
fa_program_landes: 'Dotacja państwowa',
|
||||||
|
fa_program_traeger: 'Dotacja organu prowadzącego',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user