Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1020 lines
44 KiB
TypeScript
1020 lines
44 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect } from 'react'
|
||
import { useRouter } from 'next/navigation'
|
||
import { useTheme } from '@/lib/ThemeContext'
|
||
import { useLanguage } from '@/lib/LanguageContext'
|
||
import {
|
||
useAlertsB2B,
|
||
B2BHit,
|
||
B2BTopic,
|
||
ImportanceLabel,
|
||
DecisionLabel,
|
||
getImportanceLabelColor,
|
||
getDecisionLabelColor,
|
||
formatDeadline,
|
||
getPackageIcon,
|
||
getPackageLabel
|
||
} from '@/lib/AlertsB2BContext'
|
||
import { Sidebar } from '@/components/Sidebar'
|
||
import { B2BMigrationWizard } from '@/components/B2BMigrationWizard'
|
||
import { TipBox } from '@/components/InfoBox'
|
||
import { Footer } from '@/components/Footer'
|
||
import { LanguageDropdown } from '@/components/LanguageDropdown'
|
||
import { ThemeToggle } from '@/components/ThemeToggle'
|
||
|
||
// Decision Trace Modal
|
||
function DecisionTraceModal({
|
||
hit,
|
||
onClose
|
||
}: {
|
||
hit: B2BHit
|
||
onClose: () => void
|
||
}) {
|
||
const { isDark } = useTheme()
|
||
const trace = hit.decisionTrace
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
|
||
<div className={`relative w-full max-w-xl rounded-3xl border p-6 max-h-[90vh] overflow-y-auto ${
|
||
isDark ? 'bg-slate-900 border-white/20' : 'bg-white border-slate-200 shadow-2xl'
|
||
}`}>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Decision Trace
|
||
</h3>
|
||
<button onClick={onClose} className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}>
|
||
<svg className={`w-5 h-5 ${isDark ? 'text-white/60' : 'text-slate-400'}`} 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>
|
||
|
||
{trace ? (
|
||
<div className="space-y-4">
|
||
{/* Rules Triggered */}
|
||
<div className={`p-4 rounded-xl ${isDark ? 'bg-white/5' : 'bg-slate-50'}`}>
|
||
<h4 className={`text-sm font-medium mb-2 ${isDark ? 'text-white/80' : 'text-slate-700'}`}>
|
||
Regeln ausgeloest
|
||
</h4>
|
||
<div className="flex flex-wrap gap-2">
|
||
{trace.rulesTriggered.map((rule, idx) => (
|
||
<span key={idx} className={`px-2 py-1 rounded-lg text-xs ${
|
||
isDark ? 'bg-blue-500/20 text-blue-300' : 'bg-blue-100 text-blue-700'
|
||
}`}>
|
||
{rule}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* LLM Used */}
|
||
<div className={`p-4 rounded-xl ${isDark ? 'bg-white/5' : 'bg-slate-50'}`}>
|
||
<div className="flex items-center justify-between">
|
||
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>LLM verwendet</span>
|
||
<span className={`px-2 py-1 rounded-lg text-xs ${
|
||
trace.llmUsed
|
||
? isDark ? 'bg-purple-500/20 text-purple-300' : 'bg-purple-100 text-purple-700'
|
||
: isDark ? 'bg-slate-500/20 text-slate-300' : 'bg-slate-100 text-slate-500'
|
||
}`}>
|
||
{trace.llmUsed ? `Ja (${Math.round((trace.llmConfidence || 0) * 100)}% Konfidenz)` : 'Nein'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Signals */}
|
||
<div className="space-y-3">
|
||
{/* Procurement Signals */}
|
||
{trace.signals.procurementSignalsFound.length > 0 && (
|
||
<div className={`p-4 rounded-xl ${isDark ? 'bg-green-500/10' : 'bg-green-50'}`}>
|
||
<h4 className={`text-sm font-medium mb-2 ${isDark ? 'text-green-300' : 'text-green-700'}`}>
|
||
Beschaffungs-Signale
|
||
</h4>
|
||
<p className={`text-sm ${isDark ? 'text-green-200/80' : 'text-green-600'}`}>
|
||
{trace.signals.procurementSignalsFound.join(', ')}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Public Buyer Signals */}
|
||
{trace.signals.publicBuyerSignalsFound.length > 0 && (
|
||
<div className={`p-4 rounded-xl ${isDark ? 'bg-blue-500/10' : 'bg-blue-50'}`}>
|
||
<h4 className={`text-sm font-medium mb-2 ${isDark ? 'text-blue-300' : 'text-blue-700'}`}>
|
||
Oeffentliche Auftraggeber
|
||
</h4>
|
||
<p className={`text-sm ${isDark ? 'text-blue-200/80' : 'text-blue-600'}`}>
|
||
{trace.signals.publicBuyerSignalsFound.join(', ')}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Product Signals */}
|
||
{trace.signals.productSignalsFound.length > 0 && (
|
||
<div className={`p-4 rounded-xl ${isDark ? 'bg-amber-500/10' : 'bg-amber-50'}`}>
|
||
<h4 className={`text-sm font-medium mb-2 ${isDark ? 'text-amber-300' : 'text-amber-700'}`}>
|
||
Produkt-Signale
|
||
</h4>
|
||
<p className={`text-sm ${isDark ? 'text-amber-200/80' : 'text-amber-600'}`}>
|
||
{trace.signals.productSignalsFound.join(', ')}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Negatives */}
|
||
{trace.signals.negativesFound.length > 0 && (
|
||
<div className={`p-4 rounded-xl ${isDark ? 'bg-red-500/10' : 'bg-red-50'}`}>
|
||
<h4 className={`text-sm font-medium mb-2 ${isDark ? 'text-red-300' : 'text-red-700'}`}>
|
||
Negative Signale
|
||
</h4>
|
||
<p className={`text-sm ${isDark ? 'text-red-200/80' : 'text-red-600'}`}>
|
||
{trace.signals.negativesFound.join(', ')}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<p className={`text-sm ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||
Kein Decision Trace verfuegbar.
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Email Import Modal
|
||
function EmailImportModal({
|
||
onClose,
|
||
onImport
|
||
}: {
|
||
onClose: () => void
|
||
onImport: (content: string, subject?: string) => void
|
||
}) {
|
||
const { isDark } = useTheme()
|
||
const [emailSubject, setEmailSubject] = useState('')
|
||
const [emailContent, setEmailContent] = useState('')
|
||
const [isProcessing, setIsProcessing] = useState(false)
|
||
|
||
const handleImport = async () => {
|
||
if (!emailContent.trim()) return
|
||
setIsProcessing(true)
|
||
// Simulate processing delay
|
||
await new Promise(resolve => setTimeout(resolve, 800))
|
||
onImport(emailContent, emailSubject || undefined)
|
||
setIsProcessing(false)
|
||
onClose()
|
||
}
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
|
||
<div className={`relative w-full max-w-2xl rounded-3xl border p-6 max-h-[90vh] overflow-y-auto ${
|
||
isDark ? 'bg-slate-900 border-white/20' : 'bg-white border-slate-200 shadow-2xl'
|
||
}`}>
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center text-xl">
|
||
📧
|
||
</div>
|
||
<div>
|
||
<h3 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
E-Mail manuell einfuegen
|
||
</h3>
|
||
<p className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
||
Fuegen Sie den Inhalt einer Google Alert E-Mail ein
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button onClick={onClose} className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}>
|
||
<svg className={`w-5 h-5 ${isDark ? 'text-white/60' : 'text-slate-400'}`} 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>
|
||
|
||
<div className="space-y-4">
|
||
{/* Subject (optional) */}
|
||
<div>
|
||
<label className={`block text-sm font-medium mb-2 ${isDark ? 'text-white/80' : 'text-slate-700'}`}>
|
||
Betreff (optional)
|
||
</label>
|
||
<input
|
||
type="text"
|
||
placeholder="z.B. Google Alert - Parkscheinautomaten"
|
||
value={emailSubject}
|
||
onChange={(e) => setEmailSubject(e.target.value)}
|
||
className={`w-full px-4 py-3 rounded-xl border ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20 text-white placeholder-white/40'
|
||
: 'bg-white border-slate-200 text-slate-900 placeholder-slate-400'
|
||
}`}
|
||
/>
|
||
</div>
|
||
|
||
{/* Email Content */}
|
||
<div>
|
||
<label className={`block text-sm font-medium mb-2 ${isDark ? 'text-white/80' : 'text-slate-700'}`}>
|
||
E-Mail-Inhalt *
|
||
</label>
|
||
<textarea
|
||
placeholder="Fuegen Sie hier den kompletten E-Mail-Inhalt ein...
|
||
|
||
Beispiel:
|
||
Google Alerts
|
||
|
||
Parkscheinautomaten
|
||
Tagesaktuell | 23. Januar 2026
|
||
|
||
Stadt Muenchen schreibt neue Parkscheinautomaten aus
|
||
www.muenchen.de - Die Landeshauptstadt Muenchen schreibt die Beschaffung von 150 neuen Parkscheinautomaten fuer das Stadtgebiet aus. Die Submission endet am 15.02.2026..."
|
||
value={emailContent}
|
||
onChange={(e) => setEmailContent(e.target.value)}
|
||
rows={12}
|
||
className={`w-full px-4 py-3 rounded-xl border resize-none font-mono text-sm ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20 text-white placeholder-white/30'
|
||
: 'bg-white border-slate-200 text-slate-900 placeholder-slate-400'
|
||
}`}
|
||
/>
|
||
<p className={`text-xs mt-2 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||
Kopieren Sie den gesamten E-Mail-Text inkl. Links aus Ihrer Google Alert E-Mail
|
||
</p>
|
||
</div>
|
||
|
||
{/* Info Box */}
|
||
<div className={`p-4 rounded-xl ${isDark ? 'bg-purple-500/10 border border-purple-500/30' : 'bg-purple-50 border border-purple-200'}`}>
|
||
<div className="flex items-start gap-3">
|
||
<span className="text-xl">🤖</span>
|
||
<div>
|
||
<p className={`text-sm font-medium ${isDark ? 'text-purple-300' : 'text-purple-700'}`}>
|
||
KI-Verarbeitung
|
||
</p>
|
||
<p className={`text-sm ${isDark ? 'text-purple-200/70' : 'text-purple-600'}`}>
|
||
Die KI analysiert den Inhalt, erkennt Beschaffungs-Signale, identifiziert
|
||
potenzielle Auftraggeber und bewertet die Relevanz fuer Ihr Unternehmen.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="flex items-center justify-end gap-3 mt-6 pt-4 border-t border-white/10">
|
||
<button
|
||
onClick={onClose}
|
||
className={`px-4 py-2 rounded-xl font-medium ${
|
||
isDark
|
||
? 'text-white/60 hover:text-white hover:bg-white/10'
|
||
: 'text-slate-500 hover:text-slate-900 hover:bg-slate-100'
|
||
}`}
|
||
>
|
||
Abbrechen
|
||
</button>
|
||
<button
|
||
onClick={handleImport}
|
||
disabled={!emailContent.trim() || isProcessing}
|
||
className={`px-6 py-2 rounded-xl font-medium transition-all flex items-center gap-2 ${
|
||
emailContent.trim() && !isProcessing
|
||
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:shadow-lg hover:shadow-purple-500/30'
|
||
: isDark
|
||
? 'bg-white/10 text-white/30 cursor-not-allowed'
|
||
: 'bg-slate-200 text-slate-400 cursor-not-allowed'
|
||
}`}
|
||
>
|
||
{isProcessing ? (
|
||
<>
|
||
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||
</svg>
|
||
Verarbeite...
|
||
</>
|
||
) : (
|
||
<>
|
||
<span>🔍</span>
|
||
Analysieren & Importieren
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Hit Detail Modal
|
||
function HitDetailModal({
|
||
hit,
|
||
onClose,
|
||
onFeedback
|
||
}: {
|
||
hit: B2BHit
|
||
onClose: () => void
|
||
onFeedback: (feedback: 'relevant' | 'irrelevant') => void
|
||
}) {
|
||
const { isDark } = useTheme()
|
||
const [showTrace, setShowTrace] = useState(false)
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
|
||
<div className={`relative w-full max-w-2xl rounded-3xl border p-8 max-h-[90vh] overflow-y-auto ${
|
||
isDark ? 'bg-slate-900 border-white/20' : 'bg-white border-slate-200 shadow-2xl'
|
||
}`}>
|
||
{/* Header */}
|
||
<div className="flex items-start justify-between mb-4">
|
||
<div className="flex items-center gap-3 flex-wrap">
|
||
<span className={`px-3 py-1 rounded-full text-xs font-medium border ${getImportanceLabelColor(hit.importanceLabel, isDark)}`}>
|
||
{hit.importanceLabel}
|
||
</span>
|
||
<span className={`px-2 py-1 rounded-lg text-xs ${getDecisionLabelColor(hit.decisionLabel, isDark)}`}>
|
||
{hit.decisionLabel === 'relevant' ? 'Relevant' : hit.decisionLabel === 'needs_review' ? 'Pruefung noetig' : 'Irrelevant'}
|
||
</span>
|
||
<span className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||
Score: {hit.importanceScore}
|
||
</span>
|
||
</div>
|
||
<button onClick={onClose} className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}>
|
||
<svg className={`w-5 h-5 ${isDark ? 'text-white/60' : 'text-slate-400'}`} 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>
|
||
|
||
{/* Title */}
|
||
<h2 className={`text-xl font-bold mb-4 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
{hit.title}
|
||
</h2>
|
||
|
||
{/* Metadata */}
|
||
<div className={`flex flex-wrap gap-4 mb-4 text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
||
{hit.buyerGuess && (
|
||
<span className="flex items-center gap-1">
|
||
<span>🏛️</span> {hit.buyerGuess}
|
||
</span>
|
||
)}
|
||
{hit.countryGuess && (
|
||
<span className="flex items-center gap-1">
|
||
<span>🌍</span> {hit.countryGuess}
|
||
</span>
|
||
)}
|
||
{hit.deadlineGuess && (
|
||
<span className={`flex items-center gap-1 ${
|
||
formatDeadline(hit.deadlineGuess).includes('Heute') || formatDeadline(hit.deadlineGuess).includes('Morgen')
|
||
? 'text-red-500 font-medium'
|
||
: ''
|
||
}`}>
|
||
<span>📅</span> Frist: {formatDeadline(hit.deadlineGuess)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Snippet */}
|
||
<div className={`rounded-xl p-4 mb-6 ${isDark ? 'bg-white/5' : 'bg-slate-50'}`}>
|
||
<p className={isDark ? 'text-white/80' : 'text-slate-600'}>
|
||
{hit.snippet}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Source Info */}
|
||
<div className={`flex items-center gap-4 mb-6 text-sm ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||
<span className={`px-2 py-1 rounded ${isDark ? 'bg-white/10' : 'bg-slate-100'}`}>
|
||
{hit.sourceType === 'email' ? '📧 Email' : '📡 RSS'}
|
||
</span>
|
||
<a
|
||
href={hit.originalUrl}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className={`hover:underline ${isDark ? 'text-blue-400' : 'text-blue-600'}`}
|
||
>
|
||
Original ansehen →
|
||
</a>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="flex items-center justify-between pt-4 border-t border-white/10">
|
||
{/* Feedback */}
|
||
<div className="flex items-center gap-2">
|
||
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>War das relevant?</span>
|
||
<button
|
||
onClick={() => onFeedback('relevant')}
|
||
className={`p-2 rounded-lg transition-all ${
|
||
hit.userFeedback === 'relevant'
|
||
? 'bg-green-500/20 text-green-400'
|
||
: isDark ? 'hover:bg-white/10 text-white/40' : 'hover:bg-slate-100 text-slate-400'
|
||
}`}
|
||
>
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5" />
|
||
</svg>
|
||
</button>
|
||
<button
|
||
onClick={() => onFeedback('irrelevant')}
|
||
className={`p-2 rounded-lg transition-all ${
|
||
hit.userFeedback === 'irrelevant'
|
||
? 'bg-red-500/20 text-red-400'
|
||
: isDark ? 'hover:bg-white/10 text-white/40' : 'hover:bg-slate-100 text-slate-400'
|
||
}`}
|
||
>
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14H5.236a2 2 0 01-1.789-2.894l3.5-7A2 2 0 018.736 3h4.018a2 2 0 01.485.06l3.76.94m-7 10v5a2 2 0 002 2h.096c.5 0 .905-.405.905-.904 0-.715.211-1.413.608-2.008L17 13V4m-7 10h2m5-10h2a2 2 0 012 2v6a2 2 0 01-2 2h-2.5" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Decision Trace */}
|
||
<button
|
||
onClick={() => setShowTrace(true)}
|
||
className={`px-4 py-2 rounded-lg text-sm transition-all ${
|
||
isDark
|
||
? 'bg-white/10 text-white/80 hover:bg-white/20'
|
||
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
||
}`}
|
||
>
|
||
🔍 Decision Trace
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Decision Trace Modal */}
|
||
{showTrace && (
|
||
<DecisionTraceModal hit={hit} onClose={() => setShowTrace(false)} />
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Hit Card Component
|
||
function HitCard({
|
||
hit,
|
||
onClick
|
||
}: {
|
||
hit: B2BHit
|
||
onClick: () => void
|
||
}) {
|
||
const { isDark } = useTheme()
|
||
|
||
return (
|
||
<button
|
||
onClick={onClick}
|
||
className={`w-full text-left p-4 rounded-xl transition-all group ${
|
||
isDark
|
||
? `bg-white/5 hover:bg-white/10 ${!hit.isRead ? 'border-l-4 border-amber-500' : ''}`
|
||
: `bg-slate-50 hover:bg-slate-100 ${!hit.isRead ? 'border-l-4 border-amber-500' : ''}`
|
||
}`}
|
||
>
|
||
<div className="flex items-start gap-3">
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium border ${getImportanceLabelColor(hit.importanceLabel, isDark)}`}>
|
||
{hit.importanceLabel}
|
||
</span>
|
||
<span className={`px-2 py-0.5 rounded-lg text-xs ${getDecisionLabelColor(hit.decisionLabel, isDark)}`}>
|
||
{hit.decisionLabel === 'relevant' ? 'Relevant' : hit.decisionLabel === 'needs_review' ? 'Pruefung' : 'Info'}
|
||
</span>
|
||
{hit.deadlineGuess && (
|
||
<span className={`text-xs ${
|
||
formatDeadline(hit.deadlineGuess).includes('Heute') || formatDeadline(hit.deadlineGuess).includes('Morgen')
|
||
? 'text-red-500 font-medium'
|
||
: isDark ? 'text-white/40' : 'text-slate-400'
|
||
}`}>
|
||
📅 {formatDeadline(hit.deadlineGuess)}
|
||
</span>
|
||
)}
|
||
{!hit.isRead && (
|
||
<span className="w-2 h-2 rounded-full bg-amber-500" />
|
||
)}
|
||
</div>
|
||
<h3 className={`font-medium text-sm mb-1 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
{hit.title}
|
||
</h3>
|
||
<p className={`text-xs truncate ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
||
{hit.snippet}
|
||
</p>
|
||
<div className={`flex items-center gap-3 mt-2 text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||
{hit.buyerGuess && <span>🏛️ {hit.buyerGuess}</span>}
|
||
{hit.countryGuess && <span>🌍 {hit.countryGuess}</span>}
|
||
</div>
|
||
</div>
|
||
<svg className={`w-5 h-5 flex-shrink-0 transition-colors ${
|
||
isDark ? 'text-white/30 group-hover:text-white/60' : 'text-slate-300 group-hover:text-slate-600'
|
||
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||
</svg>
|
||
</div>
|
||
</button>
|
||
)
|
||
}
|
||
|
||
// Digest View Component
|
||
function DigestView({ hits, onHitClick }: { hits: B2BHit[], onHitClick: (hit: B2BHit) => void }) {
|
||
const { isDark } = useTheme()
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<span className="text-2xl">📬</span>
|
||
<h3 className={`font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Tages-Digest (Top 10)
|
||
</h3>
|
||
</div>
|
||
{hits.length === 0 ? (
|
||
<div className={`text-center py-8 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||
<span className="text-4xl block mb-2">🎉</span>
|
||
<p>Keine relevanten Hits heute</p>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-3">
|
||
{hits.map((hit, idx) => (
|
||
<div key={hit.id} className="flex items-start gap-3">
|
||
<span className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium ${
|
||
idx < 3
|
||
? 'bg-amber-500/20 text-amber-400'
|
||
: isDark ? 'bg-white/10 text-white/60' : 'bg-slate-100 text-slate-500'
|
||
}`}>
|
||
{idx + 1}
|
||
</span>
|
||
<div className="flex-1">
|
||
<HitCard hit={hit} onClick={() => onHitClick(hit)} />
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default function AlertsB2BPage() {
|
||
const router = useRouter()
|
||
const { isDark } = useTheme()
|
||
const { t } = useLanguage()
|
||
const {
|
||
tenant,
|
||
settings,
|
||
updateSettings,
|
||
hits,
|
||
topics,
|
||
unreadCount,
|
||
relevantCount,
|
||
needsReviewCount,
|
||
markAsRead,
|
||
markAllAsRead,
|
||
submitFeedback,
|
||
processEmailContent,
|
||
getDigest
|
||
} = useAlertsB2B()
|
||
|
||
const [selectedHit, setSelectedHit] = useState<B2BHit | null>(null)
|
||
const [showWizard, setShowWizard] = useState(false)
|
||
const [showImportModal, setShowImportModal] = useState(false)
|
||
const [viewMode, setViewMode] = useState<'inbox' | 'digest'>('inbox')
|
||
const [filterDecision, setFilterDecision] = useState<string>('all')
|
||
const [filterImportance, setFilterImportance] = useState<string>('all')
|
||
const [importSuccess, setImportSuccess] = useState<B2BHit | null>(null)
|
||
|
||
// Show wizard if not completed
|
||
useEffect(() => {
|
||
if (!settings.wizardCompleted) {
|
||
setShowWizard(true)
|
||
}
|
||
}, [settings.wizardCompleted])
|
||
|
||
// Filter hits
|
||
const filteredHits = hits.filter(hit => {
|
||
if (filterDecision === 'unread') return !hit.isRead
|
||
if (filterDecision !== 'all' && hit.decisionLabel !== filterDecision) return false
|
||
if (filterImportance !== 'all' && hit.importanceLabel !== filterImportance) return false
|
||
return true
|
||
}).sort((a, b) => b.importanceScore - a.importanceScore)
|
||
|
||
// Handle hit click
|
||
const handleHitClick = (hit: B2BHit) => {
|
||
setSelectedHit(hit)
|
||
if (!hit.isRead) {
|
||
markAsRead(hit.id)
|
||
}
|
||
}
|
||
|
||
// Handle feedback
|
||
const handleFeedback = (hitId: string, feedback: 'relevant' | 'irrelevant') => {
|
||
submitFeedback(hitId, feedback)
|
||
}
|
||
|
||
// Wizard mode
|
||
if (showWizard) {
|
||
return (
|
||
<B2BMigrationWizard
|
||
onComplete={() => {
|
||
updateSettings({ wizardCompleted: true, migrationCompleted: true })
|
||
setShowWizard(false)
|
||
}}
|
||
onSkip={() => {
|
||
updateSettings({ wizardCompleted: true })
|
||
setShowWizard(false)
|
||
}}
|
||
onCancel={() => {
|
||
router.push('/')
|
||
}}
|
||
/>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className={`min-h-screen relative overflow-hidden flex flex-col ${
|
||
isDark
|
||
? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800'
|
||
: 'bg-gradient-to-br from-slate-100 via-blue-50 to-indigo-100'
|
||
}`}>
|
||
{/* Animated Background Blobs - Dashboard Style */}
|
||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||
<div className={`absolute -top-40 -right-40 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob ${
|
||
isDark ? 'bg-purple-500 opacity-70' : 'bg-purple-300 opacity-50'
|
||
}`} />
|
||
<div className={`absolute -bottom-40 -left-40 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000 ${
|
||
isDark ? 'bg-blue-500 opacity-70' : 'bg-blue-300 opacity-50'
|
||
}`} />
|
||
<div className={`absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-4000 ${
|
||
isDark ? 'bg-pink-500 opacity-70' : 'bg-pink-300 opacity-50'
|
||
}`} />
|
||
</div>
|
||
|
||
<div className="relative z-10 flex min-h-screen gap-6 p-4">
|
||
{/* Sidebar */}
|
||
<Sidebar selectedTab="alerts-b2b" />
|
||
|
||
{/* Main Content */}
|
||
<main className="flex-1">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between mb-8">
|
||
<div>
|
||
<h1 className={`text-4xl font-bold mb-2 flex items-center gap-3 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
<span>🏢</span> B2B Alerts
|
||
{unreadCount > 0 && (
|
||
<span className="px-3 py-1 text-sm font-medium rounded-full bg-purple-500/20 text-purple-400 border border-purple-500/30">
|
||
{unreadCount} neu
|
||
</span>
|
||
)}
|
||
</h1>
|
||
<p className={isDark ? 'text-white/60' : 'text-slate-600'}>
|
||
{tenant.companyName} - Procurement Alerts
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-4">
|
||
{/* View Toggle */}
|
||
<div className={`flex rounded-xl overflow-hidden border ${isDark ? 'border-white/20' : 'border-slate-200'}`}>
|
||
<button
|
||
onClick={() => setViewMode('inbox')}
|
||
className={`px-4 py-2 text-sm font-medium transition-all ${
|
||
viewMode === 'inbox'
|
||
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white'
|
||
: isDark ? 'text-white/60 hover:text-white' : 'text-slate-500 hover:text-slate-900'
|
||
}`}
|
||
>
|
||
Inbox
|
||
</button>
|
||
<button
|
||
onClick={() => setViewMode('digest')}
|
||
className={`px-4 py-2 text-sm font-medium transition-all ${
|
||
viewMode === 'digest'
|
||
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white'
|
||
: isDark ? 'text-white/60 hover:text-white' : 'text-slate-500 hover:text-slate-900'
|
||
}`}
|
||
>
|
||
Digest
|
||
</button>
|
||
</div>
|
||
|
||
<LanguageDropdown />
|
||
<ThemeToggle />
|
||
|
||
{/* Import Email Button */}
|
||
<button
|
||
onClick={() => setShowImportModal(true)}
|
||
className={`px-4 py-2 backdrop-blur-xl border rounded-2xl transition-all flex items-center gap-2 ${
|
||
isDark
|
||
? 'bg-purple-500/20 border-purple-500/30 text-purple-300 hover:bg-purple-500/30'
|
||
: 'bg-purple-100 border-purple-200 text-purple-700 hover:bg-purple-200'
|
||
}`}
|
||
>
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
<span className="text-sm font-medium">E-Mail einfuegen</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setShowWizard(true)}
|
||
className={`p-3 backdrop-blur-xl border rounded-2xl hover:bg-white/20 transition-all ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20 text-white'
|
||
: 'bg-black/5 border-black/10 text-slate-700'
|
||
}`}
|
||
>
|
||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-3 gap-6">
|
||
{/* Main Panel (2/3) */}
|
||
<div className={`col-span-2 backdrop-blur-xl border rounded-3xl p-6 ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20'
|
||
: 'bg-white/70 border-black/10 shadow-lg'
|
||
}`}>
|
||
{viewMode === 'inbox' ? (
|
||
<>
|
||
{/* Filter Bar */}
|
||
<div className="flex items-center justify-between mb-6 flex-wrap gap-3">
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
{/* Decision Filter */}
|
||
{['all', 'unread', 'relevant', 'needs_review', 'irrelevant'].map((filter) => (
|
||
<button
|
||
key={filter}
|
||
onClick={() => setFilterDecision(filter)}
|
||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${
|
||
filterDecision === filter
|
||
? 'bg-purple-500/20 text-purple-400 border border-purple-500/30'
|
||
: isDark
|
||
? 'text-white/60 hover:text-white hover:bg-white/10'
|
||
: 'text-slate-500 hover:text-slate-900 hover:bg-slate-100'
|
||
}`}
|
||
>
|
||
{filter === 'all' ? 'Alle' : filter === 'unread' ? 'Ungelesen' : filter === 'relevant' ? 'Relevant' : filter === 'needs_review' ? 'Pruefung' : 'Irrelevant'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
{unreadCount > 0 && (
|
||
<button
|
||
onClick={markAllAsRead}
|
||
className={`text-sm ${isDark ? 'text-white/60 hover:text-white' : 'text-slate-500 hover:text-slate-900'}`}
|
||
>
|
||
Alle gelesen
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Importance Filter */}
|
||
<div className="flex items-center gap-2 mb-6 flex-wrap">
|
||
<span className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>Wichtigkeit:</span>
|
||
{['all', 'KRITISCH', 'DRINGEND', 'WICHTIG', 'PRUEFEN', 'INFO'].map((filter) => (
|
||
<button
|
||
key={filter}
|
||
onClick={() => setFilterImportance(filter)}
|
||
className={`px-2 py-1 rounded text-xs transition-all ${
|
||
filterImportance === filter
|
||
? filter === 'all'
|
||
? 'bg-purple-500/20 text-purple-400'
|
||
: getImportanceLabelColor(filter as ImportanceLabel, isDark)
|
||
: isDark
|
||
? 'text-white/40 hover:text-white/60'
|
||
: 'text-slate-400 hover:text-slate-600'
|
||
}`}
|
||
>
|
||
{filter === 'all' ? 'Alle' : filter}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Hits List */}
|
||
{filteredHits.length === 0 ? (
|
||
<div className={`text-center py-12 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||
<span className="text-4xl block mb-4">📭</span>
|
||
<p>Keine Hits gefunden</p>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-3">
|
||
{filteredHits.map(hit => (
|
||
<HitCard
|
||
key={hit.id}
|
||
hit={hit}
|
||
onClick={() => handleHitClick(hit)}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
</>
|
||
) : (
|
||
<DigestView hits={getDigest()} onHitClick={handleHitClick} />
|
||
)}
|
||
</div>
|
||
|
||
{/* Right Sidebar (1/3) */}
|
||
<div className="space-y-6">
|
||
{/* Stats */}
|
||
<div className={`backdrop-blur-xl border rounded-3xl p-6 ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20'
|
||
: 'bg-white/70 border-black/10 shadow-lg'
|
||
}`}>
|
||
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
<span>📊</span> Statistik
|
||
</h2>
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>Gesamt Hits</span>
|
||
<span className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>{hits.length}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>Ungelesen</span>
|
||
<span className="font-medium text-purple-500">{unreadCount}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>Relevant</span>
|
||
<span className="font-medium text-green-500">{relevantCount}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>Pruefung noetig</span>
|
||
<span className="font-medium text-amber-500">{needsReviewCount}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Topics */}
|
||
<div className={`backdrop-blur-xl border rounded-3xl p-6 ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20'
|
||
: 'bg-white/70 border-black/10 shadow-lg'
|
||
}`}>
|
||
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
<span>🎯</span> Topics
|
||
</h2>
|
||
{topics.length === 0 ? (
|
||
<p className={`text-sm ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||
Keine Topics konfiguriert.
|
||
</p>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{topics.map(topic => (
|
||
<div
|
||
key={topic.id}
|
||
className={`flex items-center gap-3 p-3 rounded-lg ${
|
||
isDark ? 'bg-white/5' : 'bg-slate-50'
|
||
}`}
|
||
>
|
||
<span className="text-lg">{getPackageIcon(topic.package)}</span>
|
||
<div className="flex-1 min-w-0">
|
||
<p className={`font-medium text-sm truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
{topic.name}
|
||
</p>
|
||
<p className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||
{getPackageLabel(topic.package)}
|
||
</p>
|
||
</div>
|
||
<div className={`w-2 h-2 rounded-full ${topic.status === 'active' ? 'bg-green-500' : 'bg-slate-400'}`} />
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Settings Summary */}
|
||
<div className={`backdrop-blur-xl border rounded-3xl p-6 ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20'
|
||
: 'bg-white/70 border-black/10 shadow-lg'
|
||
}`}>
|
||
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
<span>⚙️</span> Einstellungen
|
||
</h2>
|
||
<div className="space-y-3 text-sm">
|
||
<div className="flex items-center justify-between">
|
||
<span className={isDark ? 'text-white/60' : 'text-slate-500'}>Benachrichtigung</span>
|
||
<span className={isDark ? 'text-white' : 'text-slate-900'}>{settings.notificationCadence}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className={isDark ? 'text-white/60' : 'text-slate-500'}>Noise-Modus</span>
|
||
<span className={`px-2 py-0.5 rounded text-xs ${
|
||
settings.noiseMode === 'STRICT'
|
||
? 'bg-green-500/20 text-green-400'
|
||
: settings.noiseMode === 'BALANCED'
|
||
? 'bg-amber-500/20 text-amber-400'
|
||
: 'bg-red-500/20 text-red-400'
|
||
}`}>
|
||
{settings.noiseMode}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className={isDark ? 'text-white/60' : 'text-slate-500'}>Max. Digest</span>
|
||
<span className={isDark ? 'text-white' : 'text-slate-900'}>{settings.maxDigestItems} Items</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Info */}
|
||
<TipBox title="Noise-Reduktion" icon="🎯">
|
||
<p className="text-sm">
|
||
Im STRICT-Modus werden nur ~10% der Hits als relevant markiert.
|
||
Der Rest wird automatisch gefiltert.
|
||
</p>
|
||
</TipBox>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
{/* Hit Detail Modal */}
|
||
{selectedHit && (
|
||
<HitDetailModal
|
||
hit={selectedHit}
|
||
onClose={() => setSelectedHit(null)}
|
||
onFeedback={(feedback) => handleFeedback(selectedHit.id, feedback)}
|
||
/>
|
||
)}
|
||
|
||
{/* Email Import Modal */}
|
||
{showImportModal && (
|
||
<EmailImportModal
|
||
onClose={() => setShowImportModal(false)}
|
||
onImport={(content, subject) => {
|
||
const newHit = processEmailContent(content, subject)
|
||
setImportSuccess(newHit)
|
||
// Auto-hide success message after 5 seconds
|
||
setTimeout(() => setImportSuccess(null), 5000)
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
{/* Import Success Notification */}
|
||
{importSuccess && (
|
||
<div className="fixed bottom-6 right-6 z-50 animate-fade-in">
|
||
<div className={`p-4 rounded-2xl border shadow-xl backdrop-blur-xl max-w-md ${
|
||
isDark
|
||
? 'bg-green-500/20 border-green-500/30'
|
||
: 'bg-green-50 border-green-200 shadow-green-100'
|
||
}`}>
|
||
<div className="flex items-start gap-3">
|
||
<span className="text-2xl">✅</span>
|
||
<div className="flex-1">
|
||
<p className={`font-medium ${isDark ? 'text-green-300' : 'text-green-700'}`}>
|
||
E-Mail erfolgreich importiert!
|
||
</p>
|
||
<p className={`text-sm mt-1 ${isDark ? 'text-green-200/70' : 'text-green-600'}`}>
|
||
{importSuccess.title.slice(0, 60)}...
|
||
</p>
|
||
<div className="flex items-center gap-2 mt-2">
|
||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
|
||
importSuccess.decisionLabel === 'relevant'
|
||
? 'bg-green-500/20 text-green-400'
|
||
: importSuccess.decisionLabel === 'needs_review'
|
||
? 'bg-amber-500/20 text-amber-400'
|
||
: 'bg-slate-500/20 text-slate-400'
|
||
}`}>
|
||
{importSuccess.decisionLabel === 'relevant' ? 'Relevant' : importSuccess.decisionLabel === 'needs_review' ? 'Pruefung' : 'Info'}
|
||
</span>
|
||
<span className={`text-xs ${isDark ? 'text-green-200/50' : 'text-green-500'}`}>
|
||
Score: {importSuccess.importanceScore}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => setImportSuccess(null)}
|
||
className={`p-1 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-green-100'}`}
|
||
>
|
||
<svg className={`w-4 h-4 ${isDark ? 'text-green-300' : 'text-green-600'}`} 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>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<Footer />
|
||
|
||
{/* Blob Animation Styles */}
|
||
<style jsx>{`
|
||
@keyframes blob {
|
||
0% { transform: translate(0px, 0px) scale(1); }
|
||
33% { transform: translate(30px, -50px) scale(1.1); }
|
||
66% { transform: translate(-20px, 20px) scale(0.9); }
|
||
100% { transform: translate(0px, 0px) scale(1); }
|
||
}
|
||
.animate-blob {
|
||
animation: blob 7s infinite;
|
||
}
|
||
.animation-delay-2000 {
|
||
animation-delay: 2s;
|
||
}
|
||
.animation-delay-4000 {
|
||
animation-delay: 4s;
|
||
}
|
||
@keyframes fade-in {
|
||
from { opacity: 0; transform: translateY(10px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
.animate-fade-in {
|
||
animation: fade-in 0.3s ease-out;
|
||
}
|
||
`}</style>
|
||
</div>
|
||
)
|
||
}
|