/ RacerTracer / Views / DashboardView.swift
DashboardView.swift
  1  //
  2  //  DashboardView.swift
  3  //  RacerTracer
  4  //
  5  //  Created by Alexander Kunau on 29.09.25.
  6  //
  7  
  8  import SwiftUI
  9  import Charts
 10  
 11  struct DashboardView: View {
 12      @EnvironmentObject var seoService: SEOAnalysisService
 13      @State private var selectedTimeRange: TimeRange = .last30Days
 14      
 15      var body: some View {
 16          ScrollView {
 17              VStack(spacing: 24) {
 18                  // Header
 19                  DashboardHeader()
 20                  
 21                  // Quick Stats Cards
 22                  StatsCardsView()
 23                  
 24                  // Charts Section
 25                  DashboardChartsSection(selectedTimeRange: $selectedTimeRange)
 26                  
 27                  // Recent Analysis
 28                  RecentAnalysisSection()
 29                  
 30                  // Issues Overview
 31                  IssuesOverviewSection()
 32              }
 33              .padding(24)
 34          }
 35          .navigationTitle("SEO Dashboard")
 36      }
 37  }
 38  
 39  struct DashboardHeader: View {
 40      var body: some View {
 41          VStack(alignment: .leading, spacing: 8) {
 42              HStack {
 43                  VStack(alignment: .leading) {
 44                      Text("SEO Dashboard")
 45                          .font(.largeTitle)
 46                          .fontWeight(.bold)
 47                      
 48                      Text("Monitor your website's SEO performance")
 49                          .font(.title3)
 50                          .foregroundColor(.secondary)
 51                  }
 52                  
 53                  Spacer()
 54                  
 55                  Button("New Analysis") {
 56                      // Action here
 57                  }
 58                  .buttonStyle(.borderedProminent)
 59              }
 60          }
 61      }
 62  }
 63  
 64  struct StatsCardsView: View {
 65      @EnvironmentObject var seoService: SEOAnalysisService
 66      
 67      var body: some View {
 68          HStack(spacing: 20) {
 69              StatsCard(
 70                  title: "Total Analyses",
 71                  value: "\(seoService.analysisHistory.count)",
 72                  icon: "chart.bar.fill",
 73                  color: .blue
 74              )
 75              
 76              StatsCard(
 77                  title: "Average Score",
 78                  value: String(format: "%.0f", averageScore),
 79                  icon: "star.fill",
 80                  color: .orange
 81              )
 82              
 83              StatsCard(
 84                  title: "Critical Issues",
 85                  value: "\(criticalIssuesCount)",
 86                  icon: "exclamationmark.triangle.fill",
 87                  color: .red
 88              )
 89              
 90              StatsCard(
 91                  title: "Last Analysis",
 92                  value: lastAnalysisDate,
 93                  icon: "clock.fill",
 94                  color: .green
 95              )
 96          }
 97      }
 98      
 99      private var averageScore: Double {
100          guard !seoService.analysisHistory.isEmpty else { return 0 }
101          let total = seoService.analysisHistory.reduce(0) { $0 + $1.overallScore }
102          return total / Double(seoService.analysisHistory.count)
103      }
104      
105      private var criticalIssuesCount: Int {
106          seoService.analysisHistory.flatMap { $0.issues }
107              .filter { $0.severity == .critical }.count
108      }
109      
110      private var lastAnalysisDate: String {
111          guard let lastAnalysis = seoService.analysisHistory.last else { return "None" }
112          let formatter = DateFormatter()
113          formatter.dateStyle = .short
114          return formatter.string(from: lastAnalysis.timestamp)
115      }
116  }
117  
118  struct StatsCard: View {
119      let title: String
120      let value: String
121      let icon: String
122      let color: Color
123      
124      var body: some View {
125          VStack(alignment: .leading, spacing: 12) {
126              HStack {
127                  Image(systemName: icon)
128                      .foregroundColor(color)
129                      .font(.title2)
130                  
131                  Spacer()
132              }
133              
134              VStack(alignment: .leading, spacing: 4) {
135                  Text(value)
136                      .font(.title)
137                      .fontWeight(.bold)
138                  
139                  Text(title)
140                      .font(.caption)
141                      .foregroundColor(.secondary)
142              }
143          }
144          .padding(16)
145          .background(Color(NSColor.controlBackgroundColor))
146          .cornerRadius(12)
147          .frame(maxWidth: .infinity)
148      }
149  }
150  
151  enum TimeRange: String, CaseIterable {
152      case last7Days = "Last 7 Days"
153      case last30Days = "Last 30 Days"
154      case last90Days = "Last 90 Days"
155  }
156  
157  struct DashboardChartsSection: View {
158      @Binding var selectedTimeRange: TimeRange
159      @EnvironmentObject var seoService: SEOAnalysisService
160      
161      var body: some View {
162          VStack(alignment: .leading, spacing: 16) {
163              HStack {
164                  Text("Performance Trends")
165                      .font(.title2)
166                      .fontWeight(.semibold)
167                  
168                  Spacer()
169                  
170                  Picker("Time Range", selection: $selectedTimeRange) {
171                      ForEach(TimeRange.allCases, id: \.self) { range in
172                          Text(range.rawValue).tag(range)
173                      }
174                  }
175                  .pickerStyle(SegmentedPickerStyle())
176                  .frame(width: 300)
177              }
178              
179              if !seoService.analysisHistory.isEmpty {
180                  Chart(filteredAnalysisData) { analysis in
181                      LineMark(
182                          x: .value("Date", analysis.timestamp),
183                          y: .value("Score", analysis.overallScore)
184                      )
185                      .foregroundStyle(Color.accentColor)
186                      .symbol(Circle())
187                  }
188                  .frame(height: 200)
189                  .chartYScale(domain: 0...100)
190                  .chartXAxis {
191                      AxisMarks(values: .stride(by: .day, count: chartDateStride)) { value in
192                          AxisValueLabel(format: .dateTime.month(.abbreviated).day())
193                          AxisGridLine()
194                          AxisTick()
195                      }
196                  }
197                  .chartYAxis {
198                      AxisMarks { value in
199                          AxisValueLabel()
200                          AxisGridLine()
201                          AxisTick()
202                      }
203                  }
204              } else {
205                  VStack(spacing: 16) {
206                      Image(systemName: "chart.line.uptrend.xyaxis")
207                          .font(.system(size: 48))
208                          .foregroundColor(.secondary)
209                      
210                      Text("No analysis data available")
211                          .font(.headline)
212                          .foregroundColor(.secondary)
213                      
214                      Text("Start by analyzing a website to see performance trends")
215                          .font(.subheadline)
216                          .foregroundColor(.secondary)
217                          .multilineTextAlignment(.center)
218                  }
219                  .frame(height: 200)
220                  .frame(maxWidth: .infinity)
221              }
222          }
223          .padding(20)
224          .background(Color(NSColor.controlBackgroundColor))
225          .cornerRadius(12)
226      }
227      
228      private var filteredAnalysisData: [SEOAnalysis] {
229          let cutoffDate: Date
230          let calendar = Calendar.current
231          
232          switch selectedTimeRange {
233          case .last7Days:
234              cutoffDate = calendar.date(byAdding: .day, value: -7, to: Date()) ?? Date()
235          case .last30Days:
236              cutoffDate = calendar.date(byAdding: .day, value: -30, to: Date()) ?? Date()
237          case .last90Days:
238              cutoffDate = calendar.date(byAdding: .day, value: -90, to: Date()) ?? Date()
239          }
240          
241          return seoService.analysisHistory.filter { $0.timestamp >= cutoffDate }
242      }
243      
244      private var chartDateStride: Int {
245          switch selectedTimeRange {
246          case .last7Days:
247              return 1
248          case .last30Days:
249              return 7
250          case .last90Days:
251              return 14
252          }
253      }
254  }
255  
256  struct RecentAnalysisSection: View {
257      @EnvironmentObject var seoService: SEOAnalysisService
258      
259      var body: some View {
260          VStack(alignment: .leading, spacing: 16) {
261              Text("Recent Analyses")
262                  .font(.title2)
263                  .fontWeight(.semibold)
264              
265              if seoService.analysisHistory.isEmpty {
266                  EmptyDashboardStateView(
267                      icon: "magnifyingglass.circle",
268                      title: "No analyses yet",
269                      subtitle: "Start analyzing websites to track their SEO performance"
270                  )
271              } else {
272                  LazyVStack(spacing: 12) {
273                      ForEach(seoService.analysisHistory.suffix(5).reversed(), id: \.id) { analysis in
274                          RecentAnalysisRow(analysis: analysis)
275                      }
276                  }
277              }
278          }
279          .padding(20)
280          .background(Color(NSColor.controlBackgroundColor))
281          .cornerRadius(12)
282      }
283  }
284  
285  struct RecentAnalysisRow: View {
286      let analysis: SEOAnalysis
287      
288      var body: some View {
289          HStack(spacing: 16) {
290              // Score indicator
291              ZStack {
292                  Circle()
293                      .strokeBorder(scoreColor.opacity(0.3), lineWidth: 3)
294                      .frame(width: 40, height: 40)
295                  
296                  Circle()
297                      .trim(from: 0, to: analysis.overallScore / 100)
298                      .stroke(scoreColor, style: StrokeStyle(lineWidth: 3, lineCap: .round))
299                      .frame(width: 40, height: 40)
300                      .rotationEffect(.degrees(-90))
301                  
302                  Text("\(Int(analysis.overallScore))")
303                      .font(.caption)
304                      .fontWeight(.semibold)
305              }
306              
307              VStack(alignment: .leading, spacing: 4) {
308                  Text(analysis.url)
309                      .font(.headline)
310                      .lineLimit(1)
311                  
312                  Text(analysis.timestamp, style: .relative)
313                      .font(.caption)
314                      .foregroundColor(.secondary)
315              }
316              
317              Spacer()
318              
319              VStack(alignment: .trailing, spacing: 4) {
320                  Text("\(analysis.issues.count) issues")
321                      .font(.caption)
322                      .foregroundColor(.secondary)
323                  
324                  if analysis.issues.contains(where: { $0.severity == .critical }) {
325                      Label("Critical", systemImage: "exclamationmark.triangle.fill")
326                          .font(.caption)
327                          .foregroundColor(.red)
328                  }
329              }
330          }
331          .padding(12)
332          .background(Color(NSColor.textBackgroundColor))
333          .cornerRadius(8)
334      }
335      
336      private var scoreColor: Color {
337          switch analysis.overallScore {
338          case 80...:
339              return .green
340          case 60..<80:
341              return .orange
342          default:
343              return .red
344          }
345      }
346  }
347  
348  struct IssuesOverviewSection: View {
349      @EnvironmentObject var seoService: SEOAnalysisService
350      
351      var body: some View {
352          VStack(alignment: .leading, spacing: 16) {
353              Text("Issues Overview")
354                  .font(.title2)
355                  .fontWeight(.semibold)
356              
357              if allIssues.isEmpty {
358                  EmptyDashboardStateView(
359                      icon: "checkmark.circle",
360                      title: "No issues found",
361                      subtitle: "Great! Your websites don't have any SEO issues"
362                  )
363              } else {
364                  LazyVStack(spacing: 8) {
365                      ForEach(issuesSummary, id: \.type) { summary in
366                          IssuesSummaryRow(summary: summary)
367                      }
368                  }
369              }
370          }
371          .padding(20)
372          .background(Color(NSColor.controlBackgroundColor))
373          .cornerRadius(12)
374      }
375      
376      private var allIssues: [SEOIssue] {
377          seoService.analysisHistory.flatMap { $0.issues }
378      }
379      
380      private var issuesSummary: [IssueSummary] {
381          let grouped = Dictionary(grouping: allIssues, by: { $0.type })
382          return grouped.map { type, issues in
383              IssueSummary(
384                  type: type,
385                  count: issues.count,
386                  severity: issues.map { $0.severity }.max() ?? .low
387              )
388          }.sorted { $0.severity.sortOrder > $1.severity.sortOrder }
389      }
390  }
391  
392  struct IssueSummary {
393      let type: SEOIssueType
394      let count: Int
395      let severity: SEOIssueSeverity
396  }
397  
398  extension SEOIssueSeverity {
399      var sortOrder: Int {
400          switch self {
401          case .critical: return 4
402          case .high: return 3
403          case .medium: return 2
404          case .low: return 1
405          }
406      }
407      
408      var color: Color {
409          switch self {
410          case .critical: return .red
411          case .high: return .orange
412          case .medium: return .yellow
413          case .low: return .blue
414          }
415      }
416  }
417  
418  struct IssuesSummaryRow: View {
419      let summary: IssueSummary
420      
421      var body: some View {
422          HStack(spacing: 12) {
423              Image(systemName: "exclamationmark.triangle.fill")
424                  .foregroundColor(summary.severity.color)
425              
426              VStack(alignment: .leading, spacing: 2) {
427                  Text(summary.type.rawValue.replacingOccurrences(of: "_", with: " ").capitalized)
428                      .font(.subheadline)
429                      .fontWeight(.medium)
430                  
431                  Text("\(summary.count) occurrence\(summary.count == 1 ? "" : "s")")
432                      .font(.caption)
433                      .foregroundColor(.secondary)
434              }
435              
436              Spacer()
437              
438              Text(summary.severity.rawValue.capitalized)
439                  .font(.caption)
440                  .fontWeight(.medium)
441                  .padding(.horizontal, 8)
442                  .padding(.vertical, 4)
443                  .background(summary.severity.color.opacity(0.2))
444                  .foregroundColor(summary.severity.color)
445                  .cornerRadius(6)
446          }
447          .padding(12)
448          .background(Color(NSColor.textBackgroundColor))
449          .cornerRadius(8)
450      }
451  }
452  
453  struct EmptyDashboardStateView: View {
454      let icon: String
455      let title: String
456      let subtitle: String
457      
458      var body: some View {
459          VStack(spacing: 16) {
460              Image(systemName: icon)
461                  .font(.system(size: 48))
462                  .foregroundColor(.secondary)
463              
464              VStack(spacing: 8) {
465                  Text(title)
466                      .font(.headline)
467                      .foregroundColor(.secondary)
468                  
469                  Text(subtitle)
470                      .font(.subheadline)
471                      .foregroundColor(.secondary)
472                      .multilineTextAlignment(.center)
473              }
474          }
475          .frame(maxWidth: .infinity)
476          .frame(height: 120)
477      }
478  }
479  
480  #Preview {
481      DashboardView()
482          .environmentObject(SEOAnalysisService())
483          .frame(width: 1000, height: 800)
484  }