/ RacerTracer / Views / PageSpeedInsightsView.swift
PageSpeedInsightsView.swift
  1  //
  2  //  PageSpeedInsightsView.swift
  3  //  RacerTracer
  4  //
  5  //  Created by Alexander Kunau on 29.09.25.
  6  //
  7  
  8  import SwiftUI
  9  import Charts
 10  
 11  struct PageSpeedInsightsView: View {
 12      @StateObject private var pageSpeedService = PageSpeedInsightsService()
 13      @State private var websiteURL = ""
 14      @State private var pageSpeedResults: PageSpeedResults?
 15      @State private var isAnalyzing = false
 16      @State private var selectedDevice: DeviceType = .mobile
 17      @State private var errorMessage: String?
 18      
 19      var body: some View {
 20          VStack(spacing: 24) {
 21              // Header with Input
 22              PageSpeedInputSection(
 23                  websiteURL: $websiteURL,
 24                  selectedDevice: $selectedDevice,
 25                  onAnalyze: performPageSpeedAnalysis,
 26                  isAnalyzing: isAnalyzing
 27              )
 28              
 29              if isAnalyzing {
 30                  PageSpeedProgressView()
 31              } else if let results = pageSpeedResults {
 32                  PageSpeedResultsView(results: results)
 33              } else if let error = errorMessage {
 34                  PageSpeedErrorView(error: error)
 35              } else {
 36                  PageSpeedStartView()
 37              }
 38          }
 39          .padding(24)
 40          .navigationTitle("PageSpeed Insights")
 41      }
 42      
 43      private func performPageSpeedAnalysis() {
 44          guard !websiteURL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
 45          guard pageSpeedService.isAPIKeyConfigured() else {
 46              errorMessage = "Please configure your PageSpeed Insights API key in Settings"
 47              return
 48          }
 49          
 50          isAnalyzing = true
 51          errorMessage = nil
 52          
 53          Task {
 54              do {
 55                  let results = try await pageSpeedService.analyzePageSpeed(
 56                      url: websiteURL,
 57                      device: selectedDevice
 58                  )
 59                  
 60                  await MainActor.run {
 61                      pageSpeedResults = results
 62                      isAnalyzing = false
 63                  }
 64              } catch {
 65                  await MainActor.run {
 66                      errorMessage = error.localizedDescription
 67                      isAnalyzing = false
 68                  }
 69              }
 70          }
 71      }
 72  }
 73  
 74  // MARK: - Input Section
 75  
 76  struct PageSpeedInputSection: View {
 77      @Binding var websiteURL: String
 78      @Binding var selectedDevice: DeviceType
 79      let onAnalyze: () -> Void
 80      let isAnalyzing: Bool
 81      
 82      var body: some View {
 83          VStack(alignment: .leading, spacing: 16) {
 84              Text("Google PageSpeed Insights Analysis")
 85                  .font(.title2)
 86                  .fontWeight(.semibold)
 87              
 88              Text("Analyze website performance using Google's PageSpeed Insights API with Core Web Vitals")
 89                  .font(.subheadline)
 90                  .foregroundColor(.secondary)
 91              
 92              VStack(spacing: 12) {
 93                  HStack {
 94                      TextField("Website URL for PageSpeed analysis", text: $websiteURL)
 95                          .textFieldStyle(RoundedBorderTextFieldStyle())
 96                          .disabled(isAnalyzing)
 97                      
 98                      Picker("Device", selection: $selectedDevice) {
 99                          ForEach(DeviceType.allCases, id: \.self) { device in
100                              Text(device.rawValue).tag(device)
101                          }
102                      }
103                      .pickerStyle(SegmentedPickerStyle())
104                      .frame(width: 200)
105                      .disabled(isAnalyzing)
106                  }
107                  
108                  Button(action: onAnalyze) {
109                      HStack {
110                          if isAnalyzing {
111                              ProgressView()
112                                  .scaleEffect(0.8)
113                          } else {
114                              Image(systemName: "speedometer")
115                          }
116                          Text(isAnalyzing ? "Analyzing Performance..." : "Analyze Performance")
117                      }
118                  }
119                  .buttonStyle(.borderedProminent)
120                  .disabled(websiteURL.isEmpty || isAnalyzing)
121              }
122          }
123          .padding(20)
124          .background(Color(NSColor.controlBackgroundColor))
125          .cornerRadius(12)
126      }
127  }
128  
129  // MARK: - Progress View
130  
131  struct PageSpeedProgressView: View {
132      var body: some View {
133          VStack(spacing: 20) {
134              ProgressView()
135                  .scaleEffect(1.5)
136              
137              Text("Running PageSpeed Analysis...")
138                  .font(.headline)
139                  .foregroundColor(.secondary)
140              
141              VStack(spacing: 8) {
142                  Text("• Measuring Core Web Vitals")
143                  Text("• Analyzing page performance")
144                  Text("• Generating optimization suggestions")
145                  Text("• Calculating performance score")
146              }
147              .font(.subheadline)
148              .foregroundColor(.secondary)
149          }
150          .frame(maxWidth: .infinity)
151          .frame(height: 250)
152          .background(Color(NSColor.controlBackgroundColor))
153          .cornerRadius(12)
154      }
155  }
156  
157  // MARK: - Start View
158  
159  struct PageSpeedStartView: View {
160      var body: some View {
161          VStack(spacing: 20) {
162              Image(systemName: "speedometer")
163                  .font(.system(size: 48))
164                  .foregroundColor(.accentColor)
165              
166              Text("PageSpeed Insights")
167                  .font(.title2)
168                  .fontWeight(.semibold)
169              
170              Text("Analyze website performance with Google's PageSpeed Insights API. Get detailed Core Web Vitals metrics and optimization suggestions.")
171                  .font(.subheadline)
172                  .foregroundColor(.secondary)
173                  .multilineTextAlignment(.center)
174          }
175          .frame(maxWidth: .infinity, maxHeight: .infinity)
176          .background(Color(NSColor.controlBackgroundColor))
177          .cornerRadius(12)
178      }
179  }
180  
181  // MARK: - Error View
182  
183  struct PageSpeedErrorView: View {
184      let error: String
185      
186      var body: some View {
187          VStack(spacing: 20) {
188              Image(systemName: "exclamationmark.triangle")
189                  .font(.system(size: 48))
190                  .foregroundColor(.orange)
191              
192              Text("Analysis Error")
193                  .font(.title2)
194                  .fontWeight(.semibold)
195              
196              Text(error)
197                  .font(.subheadline)
198                  .foregroundColor(.secondary)
199                  .multilineTextAlignment(.center)
200          }
201          .frame(maxWidth: .infinity, maxHeight: .infinity)
202          .background(Color(NSColor.controlBackgroundColor))
203          .cornerRadius(12)
204      }
205  }
206  
207  // MARK: - Results View
208  
209  struct PageSpeedResultsView: View {
210      let results: PageSpeedResults
211      
212      var body: some View {
213          ScrollView {
214              VStack(spacing: 24) {
215                  // Performance Score Overview
216                  PageSpeedScoreOverview(results: results)
217                  
218                  // Core Web Vitals
219                  CoreWebVitalsSection(metrics: results.coreWebVitals)
220                  
221                  // Performance Metrics
222                  PerformanceMetricsChart(metrics: results.performanceMetrics)
223                  
224                  // Opportunities
225                  OptimizationOpportunitiesSection(opportunities: results.opportunities)
226                  
227                  // Diagnostics
228                  DiagnosticsSection(diagnostics: results.diagnostics)
229              }
230              .padding(20)
231          }
232          .background(Color(NSColor.controlBackgroundColor))
233          .cornerRadius(12)
234      }
235  }
236  
237  // MARK: - Score Overview
238  
239  struct PageSpeedScoreOverview: View {
240      let results: PageSpeedResults
241      
242      var body: some View {
243          VStack(spacing: 16) {
244              Text("Performance Score")
245                  .font(.headline)
246                  .fontWeight(.semibold)
247              
248              ZStack {
249                  Circle()
250                      .strokeBorder(Color.secondary.opacity(0.3), lineWidth: 12)
251                      .frame(width: 120, height: 120)
252                  
253                  Circle()
254                      .trim(from: 0, to: results.performanceScore / 100)
255                      .stroke(scoreColor, style: StrokeStyle(lineWidth: 12, lineCap: .round))
256                      .frame(width: 120, height: 120)
257                      .rotationEffect(.degrees(-90))
258                  
259                  VStack(spacing: 4) {
260                      Text("\(Int(results.performanceScore))")
261                          .font(.largeTitle)
262                          .fontWeight(.bold)
263                          .foregroundColor(scoreColor)
264                      
265                      Text("Score")
266                          .font(.caption)
267                          .foregroundColor(.secondary)
268                  }
269              }
270              
271              HStack(spacing: 20) {
272                  Text("Device: \(results.device.rawValue)")
273                      .font(.subheadline)
274                      .foregroundColor(.secondary)
275                  
276                  Text("Analyzed: \(results.analysisDate, style: .time)")
277                      .font(.subheadline)
278                      .foregroundColor(.secondary)
279              }
280          }
281      }
282      
283      private var scoreColor: Color {
284          switch results.performanceScore {
285          case 90...: return .green
286          case 50..<90: return .orange
287          default: return .red
288          }
289      }
290  }
291  
292  // MARK: - Core Web Vitals
293  
294  struct CoreWebVitalsSection: View {
295      let metrics: CoreWebVitalsMetrics
296      
297      var body: some View {
298          VStack(alignment: .leading, spacing: 16) {
299              Text("Core Web Vitals")
300                  .font(.headline)
301                  .fontWeight(.semibold)
302              
303              HStack(spacing: 16) {
304                  CoreWebVitalCard(
305                      title: "LCP",
306                      subtitle: "Largest Contentful Paint",
307                      value: String(format: "%.1fs", metrics.largestContentfulPaint),
308                      status: vitalStatus(metrics.largestContentfulPaint, good: 2.5, poor: 4.0)
309                  )
310                  
311                  CoreWebVitalCard(
312                      title: "FID",
313                      subtitle: "First Input Delay",
314                      value: String(format: "%.0fms", metrics.firstInputDelay),
315                      status: vitalStatus(metrics.firstInputDelay, good: 100, poor: 300)
316                  )
317                  
318                  CoreWebVitalCard(
319                      title: "CLS",
320                      subtitle: "Cumulative Layout Shift",
321                      value: String(format: "%.3f", metrics.cumulativeLayoutShift),
322                      status: vitalStatus(metrics.cumulativeLayoutShift, good: 0.1, poor: 0.25)
323                  )
324              }
325          }
326      }
327      
328      private func vitalStatus(_ value: Double, good: Double, poor: Double) -> VitalStatus {
329          if value <= good { return .good }
330          if value <= poor { return .needsImprovement }
331          return .poor
332      }
333  }
334  
335  struct CoreWebVitalCard: View {
336      let title: String
337      let subtitle: String
338      let value: String
339      let status: VitalStatus
340      
341      var body: some View {
342          VStack(spacing: 8) {
343              Text(title)
344                  .font(.caption)
345                  .foregroundColor(.secondary)
346              
347              Text(value)
348                  .font(.title3)
349                  .fontWeight(.bold)
350                  .foregroundColor(status.color)
351              
352              Text(subtitle)
353                  .font(.caption2)
354                  .foregroundColor(.secondary)
355                  .multilineTextAlignment(.center)
356              
357              Text(status.rawValue)
358                  .font(.caption2)
359                  .padding(.horizontal, 6)
360                  .padding(.vertical, 2)
361                  .background(status.color.opacity(0.2))
362                  .foregroundColor(status.color)
363                  .cornerRadius(4)
364          }
365          .frame(maxWidth: .infinity)
366          .padding(12)
367          .background(Color(NSColor.textBackgroundColor))
368          .cornerRadius(8)
369      }
370  }
371  
372  // MARK: - Performance Metrics Chart
373  
374  struct PerformanceMetricsChart: View {
375      let metrics: PageSpeedPerformanceMetrics
376      
377      var body: some View {
378          VStack(alignment: .leading, spacing: 16) {
379              Text("Performance Metrics")
380                  .font(.headline)
381                  .fontWeight(.semibold)
382              
383              Chart(performanceData, id: \.name) { data in
384                  BarMark(
385                      x: .value("Metric", data.name),
386                      y: .value("Time", data.value)
387                  )
388                  .foregroundStyle(data.color.gradient)
389              }
390              .frame(height: 200)
391              .chartYAxis {
392                  AxisMarks { value in
393                      AxisValueLabel {
394                          if let doubleValue = value.as(Double.self) {
395                              Text("\(String(format: "%.1f", doubleValue))s")
396                          }
397                      }
398                  }
399              }
400          }
401      }
402      
403      private var performanceData: [PerformanceChartData] {
404          [
405              PerformanceChartData(name: "FCP", value: metrics.firstContentfulPaint, color: .blue),
406              PerformanceChartData(name: "LCP", value: metrics.largestContentfulPaint, color: .green),
407              PerformanceChartData(name: "TTI", value: metrics.timeToInteractive, color: .orange),
408              PerformanceChartData(name: "TBT", value: metrics.totalBlockingTime / 1000, color: .red),
409              PerformanceChartData(name: "SI", value: metrics.speedIndex, color: .purple)
410          ]
411      }
412  }
413  
414  struct PerformanceChartData {
415      let name: String
416      let value: Double
417      let color: Color
418  }
419  
420  // MARK: - Optimization Opportunities
421  
422  struct OptimizationOpportunitiesSection: View {
423      let opportunities: [OptimizationOpportunity]
424      
425      var body: some View {
426          VStack(alignment: .leading, spacing: 16) {
427              Text("Optimization Opportunities")
428                  .font(.headline)
429                  .fontWeight(.semibold)
430              
431              LazyVStack(spacing: 12) {
432                  ForEach(opportunities, id: \.id) { opportunity in
433                      OptimizationOpportunityCard(opportunity: opportunity)
434                  }
435              }
436          }
437      }
438  }
439  
440  struct OptimizationOpportunityCard: View {
441      let opportunity: OptimizationOpportunity
442      
443      var body: some View {
444          VStack(alignment: .leading, spacing: 12) {
445              HStack {
446                  Image(systemName: "lightbulb.fill")
447                      .foregroundColor(.yellow)
448                  
449                  VStack(alignment: .leading, spacing: 4) {
450                      Text(opportunity.title)
451                          .font(.subheadline)
452                          .fontWeight(.semibold)
453                      
454                      Text("Potential savings: \(String(format: "%.1f", opportunity.potentialSavings))s")
455                          .font(.caption)
456                          .foregroundColor(.green)
457                  }
458                  
459                  Spacer()
460                  
461                  Text(opportunity.impact.rawValue)
462                      .font(.caption)
463                      .padding(.horizontal, 8)
464                      .padding(.vertical, 2)
465                      .background(opportunity.impact.color.opacity(0.2))
466                      .foregroundColor(opportunity.impact.color)
467                      .cornerRadius(4)
468              }
469              
470              Text(opportunity.description)
471                  .font(.subheadline)
472                  .foregroundColor(.secondary)
473          }
474          .padding(16)
475          .background(Color(NSColor.textBackgroundColor))
476          .cornerRadius(8)
477      }
478  }
479  
480  // MARK: - Diagnostics
481  
482  struct DiagnosticsSection: View {
483      let diagnostics: [PageSpeedDiagnostic]
484      
485      var body: some View {
486          VStack(alignment: .leading, spacing: 16) {
487              Text("Diagnostics")
488                  .font(.headline)
489                  .fontWeight(.semibold)
490              
491              LazyVStack(spacing: 8) {
492                  ForEach(diagnostics, id: \.id) { diagnostic in
493                      DiagnosticCard(diagnostic: diagnostic)
494                  }
495              }
496          }
497      }
498  }
499  
500  struct DiagnosticCard: View {
501      let diagnostic: PageSpeedDiagnostic
502      
503      var body: some View {
504          HStack {
505              Image(systemName: diagnostic.status.icon)
506                  .foregroundColor(diagnostic.status.color)
507              
508              VStack(alignment: .leading, spacing: 4) {
509                  Text(diagnostic.title)
510                      .font(.subheadline)
511                      .fontWeight(.medium)
512                  
513                  Text(diagnostic.description)
514                      .font(.caption)
515                      .foregroundColor(.secondary)
516              }
517              
518              Spacer()
519          }
520          .padding(12)
521          .background(Color(NSColor.textBackgroundColor))
522          .cornerRadius(8)
523      }
524  }
525  
526  // MARK: - Data Models
527  
528  enum DeviceType: String, CaseIterable {
529      case mobile = "Mobile"
530      case desktop = "Desktop"
531  }
532  
533  struct PageSpeedResults {
534      let url: String
535      let device: DeviceType
536      let performanceScore: Double
537      let analysisDate: Date
538      let coreWebVitals: CoreWebVitalsMetrics
539      let performanceMetrics: PageSpeedPerformanceMetrics
540      let opportunities: [OptimizationOpportunity]
541      let diagnostics: [PageSpeedDiagnostic]
542  }
543  
544  struct CoreWebVitalsMetrics {
545      let largestContentfulPaint: Double
546      let firstInputDelay: Double
547      let cumulativeLayoutShift: Double
548  }
549  
550  struct PageSpeedPerformanceMetrics {
551      let firstContentfulPaint: Double
552      let largestContentfulPaint: Double
553      let timeToInteractive: Double
554      let totalBlockingTime: Double
555      let speedIndex: Double
556  }
557  
558  struct OptimizationOpportunity: Identifiable {
559      let id = UUID()
560      let title: String
561      let description: String
562      let potentialSavings: Double
563      let impact: OptimizationImpact
564  }
565  
566  enum OptimizationImpact {
567      case low
568      case medium
569      case high
570      
571      var rawValue: String {
572          switch self {
573          case .low: return "Low"
574          case .medium: return "Medium"
575          case .high: return "High"
576          }
577      }
578      
579      var color: Color {
580          switch self {
581          case .low: return .green
582          case .medium: return .orange
583          case .high: return .red
584          }
585      }
586  }
587  
588  struct PageSpeedDiagnostic: Identifiable {
589      let id = UUID()
590      let title: String
591      let description: String
592      let status: DiagnosticStatus
593  }
594  
595  enum DiagnosticStatus {
596      case passed
597      case warning
598      case failed
599      
600      var icon: String {
601          switch self {
602          case .passed: return "checkmark.circle.fill"
603          case .warning: return "exclamationmark.triangle.fill"
604          case .failed: return "xmark.circle.fill"
605          }
606      }
607      
608      var color: Color {
609          switch self {
610          case .passed: return .green
611          case .warning: return .orange
612          case .failed: return .red
613          }
614      }
615  }
616  
617  enum VitalStatus: String {
618      case good = "Good"
619      case needsImprovement = "Needs Improvement"
620      case poor = "Poor"
621      
622      var color: Color {
623          switch self {
624          case .good: return .green
625          case .needsImprovement: return .orange
626          case .poor: return .red
627          }
628      }
629  }
630  
631  #Preview {
632      PageSpeedInsightsView()
633          .environmentObject(PageSpeedInsightsService())
634          .frame(width: 800, height: 600)
635  }