cms.c2sgmbh/src/components/admin/YouTubeAnalyticsDashboard.tsx

870 lines
30 KiB
TypeScript

'use client'
import React, { useState, useEffect, useCallback } from 'react'
import './YouTubeAnalyticsDashboard.scss'
// Types
type Tab = 'performance' | 'pipeline' | 'goals' | 'community'
type Period = '7d' | '30d' | '90d'
interface Channel {
id: number
name: string
slug: string
}
interface VideoSummary {
id: number
title: string
thumbnailText: string
views: number
ctr: number
avgRetention: number
likes: number
}
interface PerformanceData {
stats: {
totalViews: number
totalWatchTimeHours: number
avgCtr: number
subscribersGained: number
}
comparison: {
views: number
watchTime: number
subscribers: number
}
topVideos: VideoSummary[]
bottomVideos: VideoSummary[]
engagement: {
likes: number
comments: number
shares: number
}
}
interface PipelineData {
stats: {
inProduction: number
thisWeekCount: number
overdueTasksCount: number
pendingApprovals: number
}
pipeline: Record<string, number>
thisWeekVideos: Array<{
id: number
title: string
status: string
scheduledPublishDate: string
channel: string
}>
overdueTasks: Array<{
id: number
title: string
dueDate: string
assignedTo: string
video: string | null
}>
}
interface GoalsData {
stats: {
contentProgress: number
contentLabel: string
subscriberProgress: number
subscriberLabel: string
viewsProgress: number
viewsLabel: string
overall: number
}
goals: any[]
customGoals: Array<{
metric: string
target: string
current: string
status: string
}>
}
interface CommunityData {
stats: {
unresolvedCount: number
positivePct: number
avgResponseHours: number
escalationsCount: number
}
sentiment: Record<string, number>
topTopics: Array<{
topic: string
count: number
}>
recentUnresolved: Array<{
id: number
message: string
authorName: string
authorHandle: string
publishedAt: string
platform: string
sentiment: string
}>
}
interface ApiResponse {
tab: string
channel: string
period: string
channels: Channel[]
data: PerformanceData | PipelineData | GoalsData | CommunityData
}
const tabConfig: Array<{ key: Tab; label: string; icon: React.ReactNode }> = [
{
key: 'performance',
label: 'Performance',
icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
),
},
{
key: 'pipeline',
label: 'Pipeline',
icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 2L2 7l10 5 10-5-10-5z" />
<path d="M2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
),
},
{
key: 'goals',
label: 'Ziele',
icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="6" />
<circle cx="12" cy="12" r="2" />
</svg>
),
},
{
key: 'community',
label: 'Community',
icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
),
},
]
const periodLabels: Record<Period, string> = {
'7d': 'Letzte 7 Tage',
'30d': 'Letzte 30 Tage',
'90d': 'Letzte 90 Tage',
}
const statusLabels: Record<string, string> = {
idea: 'Idee',
script_draft: 'Skript',
script_review: 'Review',
script_approved: 'Skript',
shoot_scheduled: 'Produktion',
shot: 'Produktion',
rough_cut: 'Schnitt',
fine_cut: 'Schnitt',
final_review: 'Review',
approved: 'Bereit',
upload_scheduled: 'Bereit',
published: 'Veröffentlicht',
}
const formatNumber = (n: number): string => {
if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`
if (n >= 1000) return `${(n / 1000).toFixed(1)}K`
return n.toString()
}
const formatDate = (dateString: string): string => {
return new Date(dateString).toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
const renderTrend = (value: number): React.ReactNode => {
if (value === 0) return null
const isUp = value > 0
return (
<span
className={`yt-analytics__trend ${isUp ? 'yt-analytics__trend--up' : 'yt-analytics__trend--down'}`}
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
{isUp ? (
<polyline points="18 15 12 9 6 15" />
) : (
<polyline points="6 9 12 15 18 9" />
)}
</svg>
{Math.abs(value)}%
</span>
)
}
export const YouTubeAnalyticsDashboard: React.FC = () => {
const [activeTab, setActiveTab] = useState<Tab>('performance')
const [period, setPeriod] = useState<Period>('30d')
const [channel, setChannel] = useState<string>('all')
const [channels, setChannels] = useState<Channel[]>([])
const [data, setData] = useState<ApiResponse['data'] | null>(null)
const [loading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null)
const fetchData = useCallback(async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(
`/api/youtube/analytics?tab=${activeTab}&channel=${channel}&period=${period}`,
{
credentials: 'include',
},
)
if (!response.ok) {
throw new Error(`API returned ${response.status}`)
}
const result: ApiResponse = await response.json()
if (result.channels && result.channels.length > 0) {
setChannels(result.channels)
}
setData(result.data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Laden der Daten')
} finally {
setLoading(false)
}
}, [activeTab, channel, period])
useEffect(() => {
fetchData()
}, [fetchData])
const renderPerformance = () => {
const perfData = data as PerformanceData
if (!perfData?.stats) return null
return (
<>
<div className="yt-analytics__stats-grid">
<div className="yt-analytics__stat-card yt-analytics__stat-card--views">
<div className="yt-analytics__stat-header">
<div className="yt-analytics__stat-icon">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
</div>
<span className="yt-analytics__stat-label">Aufrufe</span>
</div>
<div className="yt-analytics__stat-value">
{formatNumber(perfData.stats.totalViews)}
{renderTrend(perfData.comparison.views)}
</div>
</div>
<div className="yt-analytics__stat-card yt-analytics__stat-card--time">
<div className="yt-analytics__stat-header">
<div className="yt-analytics__stat-icon">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
</div>
<span className="yt-analytics__stat-label">Wiedergabezeit</span>
</div>
<div className="yt-analytics__stat-value">
{formatNumber(perfData.stats.totalWatchTimeHours)}h
{renderTrend(perfData.comparison.watchTime)}
</div>
</div>
<div className="yt-analytics__stat-card yt-analytics__stat-card--ctr">
<div className="yt-analytics__stat-header">
<div className="yt-analytics__stat-icon">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="6" />
<circle cx="12" cy="12" r="2" />
</svg>
</div>
<span className="yt-analytics__stat-label">CTR</span>
</div>
<div className="yt-analytics__stat-value">{perfData.stats.avgCtr.toFixed(1)}%</div>
</div>
<div className="yt-analytics__stat-card yt-analytics__stat-card--subs">
<div className="yt-analytics__stat-header">
<div className="yt-analytics__stat-icon">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
</div>
<span className="yt-analytics__stat-label">Neue Abos</span>
</div>
<div className="yt-analytics__stat-value">
{formatNumber(perfData.stats.subscribersGained)}
{renderTrend(perfData.comparison.subscribers)}
</div>
</div>
</div>
{perfData.topVideos && perfData.topVideos.length > 0 && (
<div className="yt-analytics__section">
<h3 className="yt-analytics__section-title">Top 5 Videos</h3>
<div className="yt-analytics__video-list">
{perfData.topVideos.map((video) => (
<div key={video.id} className="yt-analytics__video-row">
<div className="yt-analytics__video-title">{video.title}</div>
<div className="yt-analytics__video-metric">{formatNumber(video.views)} Aufrufe</div>
<div className="yt-analytics__video-metric">{video.ctr.toFixed(1)}% CTR</div>
<div className="yt-analytics__video-metric">
{video.avgRetention.toFixed(0)}% Retention
</div>
</div>
))}
</div>
</div>
)}
{perfData.bottomVideos && perfData.bottomVideos.length > 0 && (
<div className="yt-analytics__section">
<h3 className="yt-analytics__section-title">Schwächste Videos</h3>
<div className="yt-analytics__video-list">
{perfData.bottomVideos.map((video) => (
<div key={video.id} className="yt-analytics__video-row">
<div className="yt-analytics__video-title">{video.title}</div>
<div className="yt-analytics__video-metric">{formatNumber(video.views)} Aufrufe</div>
<div className="yt-analytics__video-metric">{video.ctr.toFixed(1)}% CTR</div>
<div className="yt-analytics__video-metric">
{video.avgRetention.toFixed(0)}% Retention
</div>
</div>
))}
</div>
</div>
)}
<div className="yt-analytics__section">
<h3 className="yt-analytics__section-title">Engagement</h3>
<div className="yt-analytics__engagement-grid">
<div className="yt-analytics__engagement-item">
<div className="yt-analytics__engagement-value">
{formatNumber(perfData.engagement.likes)}
</div>
<div className="yt-analytics__engagement-label">Likes</div>
</div>
<div className="yt-analytics__engagement-item">
<div className="yt-analytics__engagement-value">
{formatNumber(perfData.engagement.comments)}
</div>
<div className="yt-analytics__engagement-label">Kommentare</div>
</div>
<div className="yt-analytics__engagement-item">
<div className="yt-analytics__engagement-value">
{formatNumber(perfData.engagement.shares)}
</div>
<div className="yt-analytics__engagement-label">Shares</div>
</div>
</div>
</div>
</>
)
}
const renderPipeline = () => {
const pipeData = data as PipelineData
if (!pipeData?.pipeline) return null
// Group statuses for pipeline bar
const statusGroups: Record<string, { label: string; className: string }> = {
idea: { label: 'Idee', className: 'idea' },
script: { label: 'Skript', className: 'script' },
review: { label: 'Review', className: 'review' },
production: { label: 'Produktion', className: 'production' },
editing: { label: 'Schnitt', className: 'editing' },
ready: { label: 'Bereit', className: 'ready' },
published: { label: 'Veröffentlicht', className: 'published' },
}
const grouped = {
idea: (pipeData.pipeline.idea || 0),
script: (pipeData.pipeline.script_draft || 0) + (pipeData.pipeline.script_approved || 0),
review: (pipeData.pipeline.script_review || 0) + (pipeData.pipeline.final_review || 0),
production: (pipeData.pipeline.shoot_scheduled || 0) + (pipeData.pipeline.shot || 0),
editing: (pipeData.pipeline.rough_cut || 0) + (pipeData.pipeline.fine_cut || 0),
ready: (pipeData.pipeline.approved || 0) + (pipeData.pipeline.upload_scheduled || 0),
published: (pipeData.pipeline.published || 0),
}
const total = Object.values(grouped).reduce((sum, val) => sum + val, 0)
return (
<>
<div className="yt-analytics__stats-grid">
<div className="yt-analytics__stat-card yt-analytics__stat-card--production">
<div className="yt-analytics__stat-header">
<span className="yt-analytics__stat-label">In Produktion</span>
</div>
<div className="yt-analytics__stat-value">{pipeData.stats.inProduction}</div>
</div>
<div className="yt-analytics__stat-card yt-analytics__stat-card--success">
<div className="yt-analytics__stat-header">
<span className="yt-analytics__stat-label">Diese Woche</span>
</div>
<div className="yt-analytics__stat-value">{pipeData.stats.thisWeekCount}</div>
</div>
<div className="yt-analytics__stat-card yt-analytics__stat-card--error">
<div className="yt-analytics__stat-header">
<span className="yt-analytics__stat-label">Überfällige Tasks</span>
</div>
<div className="yt-analytics__stat-value">{pipeData.stats.overdueTasksCount}</div>
</div>
<div className="yt-analytics__stat-card yt-analytics__stat-card--warning">
<div className="yt-analytics__stat-header">
<span className="yt-analytics__stat-label">Offene Freigaben</span>
</div>
<div className="yt-analytics__stat-value">{pipeData.stats.pendingApprovals}</div>
</div>
</div>
<div className="yt-analytics__section">
<h3 className="yt-analytics__section-title">Pipeline Übersicht</h3>
<div className="yt-analytics__pipeline-legend">
{Object.entries(statusGroups).map(([key, { label, className }]) => (
<div key={key} className="yt-analytics__legend-item">
<div className={`yt-analytics__legend-dot yt-analytics__pipeline-segment--${className}`} />
<span>{label}</span>
</div>
))}
</div>
<div className="yt-analytics__pipeline-bar">
{Object.entries(grouped).map(([key, count]) => {
const percentage = total > 0 ? (count / total) * 100 : 0
const { label, className } = statusGroups[key]
return count > 0 ? (
<div
key={key}
className={`yt-analytics__pipeline-segment yt-analytics__pipeline-segment--${className}`}
style={{ flexBasis: `${percentage}%` }}
title={`${label}: ${count}`}
>
{percentage > 5 && `${count}`}
</div>
) : null
})}
</div>
</div>
{pipeData.thisWeekVideos && pipeData.thisWeekVideos.length > 0 && (
<div className="yt-analytics__section">
<h3 className="yt-analytics__section-title">Nächste Veröffentlichungen</h3>
{pipeData.thisWeekVideos.map((video) => (
<div key={video.id} className="yt-analytics__schedule-item">
<div className="yt-analytics__schedule-title">{video.title}</div>
<div className="yt-analytics__schedule-date">
{formatDate(video.scheduledPublishDate)} {video.channel}
</div>
</div>
))}
</div>
)}
{pipeData.overdueTasks && pipeData.overdueTasks.length > 0 && (
<div className="yt-analytics__section">
<h3 className="yt-analytics__section-title">Überfällige Aufgaben</h3>
{pipeData.overdueTasks.map((task) => (
<div key={task.id} className="yt-analytics__task-item">
<div className="yt-analytics__task-header">
<div className="yt-analytics__task-title">{task.title}</div>
<div className="yt-analytics__task-meta">Fällig: {formatDate(task.dueDate)}</div>
</div>
<div className="yt-analytics__task-detail">
{task.assignedTo}
{task.video && `${task.video}`}
</div>
</div>
))}
</div>
)}
</>
)
}
const renderGoals = () => {
const goalsData = data as GoalsData
if (!goalsData?.stats) return null
return (
<>
<div className="yt-analytics__stats-grid">
<div className="yt-analytics__stat-card yt-analytics__stat-card--production">
<div className="yt-analytics__stat-header">
<span className="yt-analytics__stat-label">Content</span>
</div>
<div className="yt-analytics__stat-value">{goalsData.stats.contentLabel}</div>
</div>
<div className="yt-analytics__stat-card yt-analytics__stat-card--subs">
<div className="yt-analytics__stat-header">
<span className="yt-analytics__stat-label">Abonnenten</span>
</div>
<div className="yt-analytics__stat-value">{goalsData.stats.subscriberLabel}</div>
</div>
<div className="yt-analytics__stat-card yt-analytics__stat-card--views">
<div className="yt-analytics__stat-header">
<span className="yt-analytics__stat-label">Aufrufe</span>
</div>
<div className="yt-analytics__stat-value">{goalsData.stats.viewsLabel}</div>
</div>
<div className="yt-analytics__stat-card yt-analytics__stat-card--success">
<div className="yt-analytics__stat-header">
<span className="yt-analytics__stat-label">Gesamt</span>
</div>
<div className="yt-analytics__stat-value">{goalsData.stats.overall.toFixed(0)}%</div>
</div>
</div>
<div className="yt-analytics__section">
<h3 className="yt-analytics__section-title">Fortschritt</h3>
<div className="yt-analytics__progress-item">
<div className="yt-analytics__progress-label">Content</div>
<div className="yt-analytics__progress-bar">
<div
className="yt-analytics__progress-fill"
style={{ width: `${Math.min(goalsData.stats.contentProgress, 100)}%` }}
/>
</div>
<div className="yt-analytics__progress-value">{goalsData.stats.contentLabel}</div>
</div>
<div className="yt-analytics__progress-item">
<div className="yt-analytics__progress-label">Abonnenten</div>
<div className="yt-analytics__progress-bar">
<div
className="yt-analytics__progress-fill"
style={{ width: `${Math.min(goalsData.stats.subscriberProgress, 100)}%` }}
/>
</div>
<div className="yt-analytics__progress-value">{goalsData.stats.subscriberLabel}</div>
</div>
<div className="yt-analytics__progress-item">
<div className="yt-analytics__progress-label">Aufrufe</div>
<div className="yt-analytics__progress-bar">
<div
className="yt-analytics__progress-fill"
style={{ width: `${Math.min(goalsData.stats.viewsProgress, 100)}%` }}
/>
</div>
<div className="yt-analytics__progress-value">{goalsData.stats.viewsLabel}</div>
</div>
</div>
{goalsData.customGoals && goalsData.customGoals.length > 0 && (
<div className="yt-analytics__section">
<h3 className="yt-analytics__section-title">Benutzerdefinierte Ziele</h3>
{goalsData.customGoals.map((goal, index) => (
<div key={index} className="yt-analytics__video-row">
<div className="yt-analytics__video-title">{goal.metric}</div>
<div className="yt-analytics__video-metric">
{goal.current} / {goal.target}
</div>
<span className={`yt-analytics__status-badge yt-analytics__status-badge--${goal.status}`}>
{goal.status === 'on_track' && 'Im Plan'}
{goal.status === 'at_risk' && 'Gefährdet'}
{goal.status === 'achieved' && 'Erreicht'}
{goal.status === 'missed' && 'Verfehlt'}
</span>
</div>
))}
</div>
)}
</>
)
}
const renderCommunity = () => {
const commData = data as CommunityData
if (!commData?.stats) return null
const sentimentMax = Math.max(...Object.values(commData.sentiment))
return (
<>
<div className="yt-analytics__stats-grid">
<div className="yt-analytics__stat-card yt-analytics__stat-card--warning">
<div className="yt-analytics__stat-header">
<span className="yt-analytics__stat-label">Unbearbeitet</span>
</div>
<div className="yt-analytics__stat-value">{commData.stats.unresolvedCount}</div>
</div>
<div className="yt-analytics__stat-card yt-analytics__stat-card--success">
<div className="yt-analytics__stat-header">
<span className="yt-analytics__stat-label">Positiv</span>
</div>
<div className="yt-analytics__stat-value">{commData.stats.positivePct.toFixed(0)}%</div>
</div>
<div className="yt-analytics__stat-card yt-analytics__stat-card--production">
<div className="yt-analytics__stat-header">
<span className="yt-analytics__stat-label">Antwortzeit</span>
</div>
<div className="yt-analytics__stat-value">{commData.stats.avgResponseHours.toFixed(1)}h</div>
</div>
<div className="yt-analytics__stat-card yt-analytics__stat-card--error">
<div className="yt-analytics__stat-header">
<span className="yt-analytics__stat-label">Eskalationen</span>
</div>
<div className="yt-analytics__stat-value">{commData.stats.escalationsCount}</div>
</div>
</div>
<div className="yt-analytics__section">
<h3 className="yt-analytics__section-title">Sentiment</h3>
<div className="yt-analytics__sentiment-list">
{Object.entries(commData.sentiment).map(([sentiment, count]) => {
const percentage = sentimentMax > 0 ? (count / sentimentMax) * 100 : 0
return (
<div key={sentiment} className="yt-analytics__sentiment-item">
<div className="yt-analytics__sentiment-label">
{sentiment === 'positive' && 'Positiv'}
{sentiment === 'neutral' && 'Neutral'}
{sentiment === 'negative' && 'Negativ'}
{sentiment === 'question' && 'Frage'}
</div>
<div className="yt-analytics__sentiment-bar-track">
<div
className={`yt-analytics__sentiment-bar-fill yt-analytics__sentiment-bar-fill--${sentiment}`}
style={{ width: `${percentage}%` }}
/>
</div>
<div className="yt-analytics__sentiment-count">{count}</div>
</div>
)
})}
</div>
</div>
{commData.topTopics && commData.topTopics.length > 0 && (
<div className="yt-analytics__section">
<h3 className="yt-analytics__section-title">Top-Themen</h3>
<div className="yt-analytics__topic-list">
{commData.topTopics.map((topic, index) => (
<div key={index} className="yt-analytics__topic-tag">
{topic.topic}
<span className="yt-analytics__topic-count">{topic.count}</span>
</div>
))}
</div>
</div>
)}
{commData.recentUnresolved && commData.recentUnresolved.length > 0 && (
<div className="yt-analytics__section">
<h3 className="yt-analytics__section-title">Neueste unbearbeitete Kommentare</h3>
{commData.recentUnresolved.map((comment) => (
<div key={comment.id} className="yt-analytics__comment-item">
<div className="yt-analytics__comment-header">
<div className="yt-analytics__comment-author">
{comment.authorName} (@{comment.authorHandle})
</div>
<div className="yt-analytics__comment-meta">
{comment.platform} {formatDate(comment.publishedAt)}
</div>
</div>
<div className="yt-analytics__comment-message">{comment.message}</div>
</div>
))}
</div>
)}
</>
)
}
return (
<div className="yt-analytics">
<div className="yt-analytics__header">
<div className="yt-analytics__title-section">
<h2>YouTube Analytics</h2>
<p>Übersicht über Performance, Pipeline, Ziele und Community</p>
</div>
<div className="yt-analytics__controls">
<select
className="yt-analytics__channel-select"
value={channel}
onChange={(e) => setChannel(e.target.value)}
>
<option value="all">Alle Kanäle</option>
{channels.map((ch) => (
<option key={ch.id} value={ch.id}>
{ch.name}
</option>
))}
</select>
<select
className="yt-analytics__period-select"
value={period}
onChange={(e) => setPeriod(e.target.value as Period)}
>
{Object.entries(periodLabels).map(([key, label]) => (
<option key={key} value={key}>
{label}
</option>
))}
</select>
<button
className="yt-analytics__refresh-btn"
onClick={fetchData}
disabled={loading}
aria-label="Aktualisieren"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
className={loading ? 'yt-analytics__spinner' : ''}
>
<path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16" />
</svg>
</button>
</div>
</div>
<div className="yt-analytics__tabs">
{tabConfig.map((tab) => (
<button
key={tab.key}
className={`yt-analytics__tab ${activeTab === tab.key ? 'yt-analytics__tab--active' : ''}`}
onClick={() => {
if (tab.key !== activeTab) {
setData(null)
setActiveTab(tab.key)
}
}}
>
{tab.icon}
{tab.label}
</button>
))}
</div>
{loading && !data && (
<div className="yt-analytics__loading">
<svg
width="40"
height="40"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
className="yt-analytics__spinner"
>
<path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16" />
</svg>
<p>Lade Daten...</p>
</div>
)}
{error && (
<div className="yt-analytics__error">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<p>{error}</p>
</div>
)}
{!loading && !error && data && (
<div className="yt-analytics__content">
{activeTab === 'performance' && renderPerformance()}
{activeTab === 'pipeline' && renderPipeline()}
{activeTab === 'goals' && renderGoals()}
{activeTab === 'community' && renderCommunity()}
</div>
)}
</div>
)
}
export default YouTubeAnalyticsDashboard