/ RacerTracer / Views / URLAnalysisView.swift
URLAnalysisView.swift
   1  //
   2  //  URLAnalysisView.swift
   3  //  RacerTracer
   4  //
   5  //  Created by Alexander Kunau on 29.09.25.
   6  //
   7  
   8  import SwiftUI
   9  import Charts
  10  
  11  struct URLAnalysisView: View {
  12      @EnvironmentObject var seoService: SEOAnalysisService
  13      @State private var urlToAnalyze = ""
  14      @State private var showingAnalysisResults = false
  15      @State private var errorAlert = false
  16      @State private var errorMessage = ""
  17      
  18      var body: some View {
  19          VStack(spacing: 24) {
  20              // URL Input Section
  21              URLInputSection(
  22                  urlToAnalyze: $urlToAnalyze,
  23                  onAnalyze: performAnalysis
  24              )
  25              
  26              // Analysis Progress
  27              if seoService.isAnalyzing {
  28                  AnalysisProgressView()
  29              }
  30              
  31              // Results Section
  32              if let analysis = seoService.currentAnalysis, !seoService.isAnalyzing {
  33                  AnalysisResultsView(analysis: analysis)
  34              } else if !seoService.isAnalyzing {
  35                  EmptyAnalysisView()
  36              }
  37          }
  38          .padding(24)
  39          .navigationTitle("URL Analysis")
  40          .alert("Analysis Error", isPresented: $errorAlert) {
  41              Button("OK") { }
  42          } message: {
  43              Text(errorMessage)
  44          }
  45      }
  46      
  47      private func performAnalysis() {
  48          guard !urlToAnalyze.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
  49              errorMessage = "Please enter a valid URL"
  50              errorAlert = true
  51              return
  52          }
  53          
  54          Task {
  55              do {
  56                  _ = try await seoService.analyzeWebsite(url: urlToAnalyze)
  57                  showingAnalysisResults = true
  58              } catch {
  59                  await MainActor.run {
  60                      errorMessage = error.localizedDescription
  61                      errorAlert = true
  62                  }
  63              }
  64          }
  65      }
  66  }
  67  
  68  struct URLInputSection: View {
  69      @Binding var urlToAnalyze: String
  70      let onAnalyze: () -> Void
  71      
  72      var body: some View {
  73          VStack(alignment: .leading, spacing: 16) {
  74              Text("Website Analysis")
  75                  .font(.title2)
  76                  .fontWeight(.semibold)
  77              
  78              Text("Enter a URL to analyze its SEO performance, keyword usage, and technical implementation.")
  79                  .font(.subheadline)
  80                  .foregroundColor(.secondary)
  81              
  82              HStack(spacing: 12) {
  83                  TextField("Enter website URL (e.g., https://example.com)", text: $urlToAnalyze)
  84                      .textFieldStyle(RoundedBorderTextFieldStyle())
  85                      .onSubmit {
  86                          onAnalyze()
  87                      }
  88                  
  89                  Button(action: onAnalyze) {
  90                      HStack {
  91                          Image(systemName: "magnifyingglass")
  92                          Text("Analyze")
  93                      }
  94                  }
  95                  .buttonStyle(.borderedProminent)
  96                  .disabled(urlToAnalyze.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
  97              }
  98          }
  99          .padding(20)
 100          .background(Color(NSColor.controlBackgroundColor))
 101          .cornerRadius(12)
 102      }
 103  }
 104  
 105  struct AnalysisProgressView: View {
 106      @EnvironmentObject var seoService: SEOAnalysisService
 107      
 108      var body: some View {
 109          VStack(spacing: 20) {
 110              Text("Analyzing Website...")
 111                  .font(.title3)
 112                  .fontWeight(.semibold)
 113              
 114              ProgressView(value: seoService.progress, total: 1.0)
 115                  .progressViewStyle(LinearProgressViewStyle())
 116                  .frame(width: 300)
 117              
 118              Text("\(Int(seoService.progress * 100))% Complete")
 119                  .font(.caption)
 120                  .foregroundColor(.secondary)
 121              
 122              VStack(alignment: .leading, spacing: 8) {
 123                  ProgressStep(
 124                      title: "Fetching HTML Content",
 125                      isComplete: seoService.progress > 0.1,
 126                      isActive: seoService.progress <= 0.3
 127                  )
 128                  
 129                  ProgressStep(
 130                      title: "Analyzing Page Structure",
 131                      isComplete: seoService.progress > 0.3,
 132                      isActive: seoService.progress > 0.1 && seoService.progress <= 0.5
 133                  )
 134                  
 135                  ProgressStep(
 136                      title: "Extracting Keywords",
 137                      isComplete: seoService.progress > 0.5,
 138                      isActive: seoService.progress > 0.3 && seoService.progress <= 0.7
 139                  )
 140                  
 141                  ProgressStep(
 142                      title: "Technical SEO Audit",
 143                      isComplete: seoService.progress > 0.7,
 144                      isActive: seoService.progress > 0.5 && seoService.progress <= 0.9
 145                  )
 146                  
 147                  ProgressStep(
 148                      title: "PageSpeed Insights",
 149                      isComplete: seoService.progress > 0.9,
 150                      isActive: seoService.progress > 0.7
 151                  )
 152              }
 153          }
 154          .padding(40)
 155          .background(Color(NSColor.controlBackgroundColor))
 156          .cornerRadius(12)
 157      }
 158  }
 159  
 160  struct ProgressStep: View {
 161      let title: String
 162      let isComplete: Bool
 163      let isActive: Bool
 164      
 165      var body: some View {
 166          HStack(spacing: 12) {
 167              Image(systemName: isComplete ? "checkmark.circle.fill" : (isActive ? "circle.dotted" : "circle"))
 168                  .foregroundColor(isComplete ? .green : (isActive ? .blue : .secondary))
 169                  .font(.title3)
 170              
 171              Text(title)
 172                  .font(.subheadline)
 173                  .foregroundColor(isActive || isComplete ? .primary : .secondary)
 174          }
 175      }
 176  }
 177  
 178  struct AnalysisResultsView: View {
 179      let analysis: SEOAnalysis
 180      
 181      var body: some View {
 182          VStack(spacing: 0) {
 183              // Results Header
 184              AnalysisResultsHeader(analysis: analysis)
 185              
 186              // Native macOS TabView
 187              TabView {
 188                  OverviewTabContent(analysis: analysis)
 189                      .tabItem {
 190                          Label("Overview", systemImage: "chart.bar.fill")
 191                      }
 192                  
 193                  KeywordsTabContent(analysis: analysis)
 194                      .tabItem {
 195                          Label("Keywords", systemImage: "key.fill")
 196                      }
 197                  
 198                  TechnicalTabContent(analysis: analysis)
 199                      .tabItem {
 200                          Label("Technical", systemImage: "gearshape.fill")
 201                      }
 202                  
 203                  IssuesTabContent(analysis: analysis)
 204                      .tabItem {
 205                          Label("Issues", systemImage: "exclamationmark.triangle.fill")
 206                      }
 207              }
 208              .background(Color(NSColor.controlBackgroundColor))
 209          }
 210          .background(Color(NSColor.controlBackgroundColor))
 211          .cornerRadius(12)
 212      }
 213  }
 214  
 215  
 216  
 217  struct AnalysisResultsHeader: View {
 218      let analysis: SEOAnalysis
 219      
 220      var body: some View {
 221          HStack(spacing: 20) {
 222              VStack(alignment: .leading, spacing: 8) {
 223                  Text(analysis.url)
 224                      .font(.title2)
 225                      .fontWeight(.semibold)
 226                      .lineLimit(1)
 227                  
 228                  Text("Analyzed \(analysis.timestamp, style: .relative)")
 229                      .font(.caption)
 230                      .foregroundColor(.secondary)
 231              }
 232              
 233              Spacer()
 234              
 235              // Overall Score Circle
 236              ZStack {
 237                  Circle()
 238                      .strokeBorder(Color.secondary.opacity(0.3), lineWidth: 8)
 239                      .frame(width: 80, height: 80)
 240                  
 241                  Circle()
 242                      .trim(from: 0, to: analysis.overallScore / 100)
 243                      .stroke(scoreColor, style: StrokeStyle(lineWidth: 8, lineCap: .round))
 244                      .frame(width: 80, height: 80)
 245                      .rotationEffect(.degrees(-90))
 246                  
 247                  VStack(spacing: 2) {
 248                      Text("\(Int(analysis.overallScore))")
 249                          .font(.title)
 250                          .fontWeight(.bold)
 251                      
 252                      Text("Score")
 253                          .font(.caption)
 254                          .foregroundColor(.secondary)
 255                  }
 256              }
 257          }
 258          .padding(20)
 259          .background(Color(NSColor.textBackgroundColor))
 260      }
 261      
 262      private var scoreColor: Color {
 263          switch analysis.overallScore {
 264          case 80...:
 265              return .green
 266          case 60..<80:
 267              return .orange
 268          default:
 269              return .red
 270          }
 271      }
 272  }
 273  
 274  
 275  
 276  struct OverviewTabContent: View {
 277      let analysis: SEOAnalysis
 278      
 279      var body: some View {
 280          ScrollView {
 281              VStack(spacing: 20) {
 282                  // Score Breakdown
 283                  ScoreBreakdownView(analysis: analysis)
 284                  
 285                  // Key Metrics
 286                  KeyMetricsView(analysis: analysis)
 287                  
 288                  // PageSpeed Insights Summary
 289                  if let pageSpeedInsights = analysis.pageSpeedInsights {
 290                      PageSpeedInsightsSummaryView(insights: pageSpeedInsights)
 291                  }
 292              }
 293              .padding(20)
 294          }
 295      }
 296  }
 297  
 298  struct ScoreBreakdownView: View {
 299      let analysis: SEOAnalysis
 300      
 301      var body: some View {
 302          VStack(alignment: .leading, spacing: 16) {
 303              Text("Score Breakdown")
 304                  .font(.headline)
 305                  .fontWeight(.semibold)
 306              
 307              LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 16) {
 308                  ScoreCard(title: "Title Tag", score: analysis.metrics.titleTag.score, color: .blue)
 309                  ScoreCard(title: "Meta Description", score: analysis.metrics.metaDescription.score, color: .green)
 310                  ScoreCard(title: "Headings", score: analysis.metrics.headings.score, color: .orange)
 311                  ScoreCard(title: "Images", score: analysis.metrics.images.score, color: .purple)
 312                  ScoreCard(title: "Keywords", score: analysis.metrics.keywords.score, color: .red)
 313                  ScoreCard(title: "Technical SEO", score: analysis.metrics.technicalSEO.score, color: .teal)
 314              }
 315          }
 316      }
 317  }
 318  
 319  struct ScoreCard: View {
 320      let title: String
 321      let score: Double
 322      let color: Color
 323      
 324      var body: some View {
 325          VStack(spacing: 8) {
 326              Text(title)
 327                  .font(.caption)
 328                  .foregroundColor(.secondary)
 329                  .multilineTextAlignment(.center)
 330              
 331              ZStack {
 332                  Circle()
 333                      .strokeBorder(Color.secondary.opacity(0.3), lineWidth: 4)
 334                      .frame(width: 40, height: 40)
 335                  
 336                  Circle()
 337                      .trim(from: 0, to: score / 100)
 338                      .stroke(color, style: StrokeStyle(lineWidth: 4, lineCap: .round))
 339                      .frame(width: 40, height: 40)
 340                      .rotationEffect(.degrees(-90))
 341                  
 342                  Text("\(Int(score))")
 343                      .font(.caption)
 344                      .fontWeight(.semibold)
 345              }
 346          }
 347          .padding(12)
 348          .background(Color(NSColor.textBackgroundColor))
 349          .cornerRadius(8)
 350      }
 351  }
 352  
 353  struct KeyMetricsView: View {
 354      let analysis: SEOAnalysis
 355      
 356      var body: some View {
 357          VStack(alignment: .leading, spacing: 16) {
 358              Text("Key Metrics")
 359                  .font(.headline)
 360                  .fontWeight(.semibold)
 361              
 362              VStack(spacing: 12) {
 363                  MetricRow(
 364                      label: "Title Length",
 365                      value: "\(analysis.metrics.titleTag.length) characters",
 366                      status: analysis.metrics.titleTag.isOptimal ? .good : .warning
 367                  )
 368                  
 369                  MetricRow(
 370                      label: "Meta Description Length",
 371                      value: "\(analysis.metrics.metaDescription.length) characters",
 372                      status: analysis.metrics.metaDescription.isOptimal ? .good : .warning
 373                  )
 374                  
 375                  MetricRow(
 376                      label: "H1 Tags",
 377                      value: "\(analysis.metrics.headings.h1Count)",
 378                      status: analysis.metrics.headings.h1Count == 1 ? .good : .warning
 379                  )
 380                  
 381                  MetricRow(
 382                      label: "Internal Links",
 383                      value: "\(analysis.metrics.internalLinks.count)",
 384                      status: analysis.metrics.internalLinks.count > 0 ? .good : .info
 385                  )
 386                  
 387                  MetricRow(
 388                      label: "External Links",
 389                      value: "\(analysis.metrics.externalLinks.count)",
 390                      status: analysis.metrics.externalLinks.count > 0 ? .good : .info
 391                  )
 392                  
 393                  MetricRow(
 394                      label: "Images without Alt Text",
 395                      value: "\(analysis.metrics.images.imagesWithoutAlt)",
 396                      status: analysis.metrics.images.imagesWithoutAlt == 0 ? .good : .error
 397                  )
 398              }
 399          }
 400      }
 401  }
 402  
 403  enum MetricStatus {
 404      case good
 405      case warning
 406      case error
 407      case info
 408      
 409      var color: Color {
 410          switch self {
 411          case .good: return .green
 412          case .warning: return .orange
 413          case .error: return .red
 414          case .info: return .blue
 415          }
 416      }
 417      
 418      var icon: String {
 419          switch self {
 420          case .good: return "checkmark.circle.fill"
 421          case .warning: return "exclamationmark.triangle.fill"
 422          case .error: return "xmark.circle.fill"
 423          case .info: return "info.circle.fill"
 424          }
 425      }
 426  }
 427  
 428  struct MetricRow: View {
 429      let label: String
 430      let value: String
 431      let status: MetricStatus
 432      
 433      var body: some View {
 434          HStack {
 435              Text(label)
 436                  .font(.subheadline)
 437              
 438              Spacer()
 439              
 440              HStack(spacing: 8) {
 441                  Text(value)
 442                      .font(.subheadline)
 443                      .fontWeight(.medium)
 444                  
 445                  Image(systemName: status.icon)
 446                      .foregroundColor(status.color)
 447              }
 448          }
 449          .padding(.vertical, 4)
 450      }
 451  }
 452  
 453  struct KeywordsTabContent: View {
 454      let analysis: SEOAnalysis
 455      
 456      var body: some View {
 457          ScrollView {
 458              VStack(spacing: 20) {
 459                  // Keyword Overview
 460                  KeywordOverviewView(keywords: analysis.metrics.keywords)
 461                  
 462                  // Keyword Distribution
 463                  KeywordDistributionView(distribution: analysis.metrics.keywords.keywordDistribution)
 464                  
 465                  // TF-IDF Scores
 466                  TFIDFScoresView(scores: analysis.metrics.keywords.tfIdfScores)
 467              }
 468              .padding(20)
 469          }
 470      }
 471  }
 472  
 473  struct KeywordOverviewView: View {
 474      let keywords: KeywordAnalysis
 475      
 476      var body: some View {
 477          VStack(alignment: .leading, spacing: 16) {
 478              Text("Keyword Overview")
 479                  .font(.headline)
 480                  .fontWeight(.semibold)
 481              
 482              HStack(spacing: 20) {
 483                  KeywordCategoryCard(
 484                      title: "Primary Keywords",
 485                      count: keywords.primaryKeywords.count,
 486                      color: .red
 487                  )
 488                  
 489                  KeywordCategoryCard(
 490                      title: "Secondary Keywords",
 491                      count: keywords.secondaryKeywords.count,
 492                      color: .orange
 493                  )
 494                  
 495                  KeywordCategoryCard(
 496                      title: "Long-tail Keywords",
 497                      count: keywords.longtailKeywords.count,
 498                      color: .blue
 499                  )
 500              }
 501              
 502              VStack(alignment: .leading, spacing: 8) {
 503                  Text("Keyword Density: \(String(format: "%.2f%%", keywords.keywordDensity * 100))")
 504                      .font(.subheadline)
 505                  
 506                  ProgressView(value: keywords.keywordDensity, total: 0.05) // 5% max
 507                      .progressViewStyle(LinearProgressViewStyle(tint: keywords.keywordDensity > 0.03 ? .red : .green))
 508              }
 509          }
 510      }
 511  }
 512  
 513  struct KeywordCategoryCard: View {
 514      let title: String
 515      let count: Int
 516      let color: Color
 517      
 518      var body: some View {
 519          VStack(spacing: 8) {
 520              Text("\(count)")
 521                  .font(.title)
 522                  .fontWeight(.bold)
 523                  .foregroundColor(color)
 524              
 525              Text(title)
 526                  .font(.caption)
 527                  .foregroundColor(.secondary)
 528                  .multilineTextAlignment(.center)
 529          }
 530          .frame(maxWidth: .infinity)
 531          .padding(16)
 532          .background(Color(NSColor.textBackgroundColor))
 533          .cornerRadius(8)
 534      }
 535  }
 536  
 537  struct KeywordDistributionView: View {
 538      let distribution: [KeywordDistribution]
 539      
 540      var body: some View {
 541          VStack(alignment: .leading, spacing: 16) {
 542              Text("Keyword Distribution")
 543                  .font(.headline)
 544                  .fontWeight(.semibold)
 545              
 546              LazyVStack(spacing: 8) {
 547                  ForEach(distribution.prefix(10), id: \.id) { item in
 548                      KeywordDistributionRow(distribution: item)
 549                  }
 550              }
 551          }
 552      }
 553  }
 554  
 555  struct KeywordDistributionRow: View {
 556      let distribution: KeywordDistribution
 557      
 558      var body: some View {
 559          VStack(alignment: .leading, spacing: 8) {
 560              Text(distribution.keyword)
 561                  .font(.subheadline)
 562                  .fontWeight(.medium)
 563              
 564              HStack(spacing: 12) {
 565                  DistributionIndicator(label: "Title", isPresent: distribution.titlePresence)
 566                  DistributionIndicator(label: "H1", isPresent: distribution.h1Presence)
 567                  DistributionIndicator(label: "H2", isPresent: distribution.h2Presence)
 568                  DistributionIndicator(label: "Meta", isPresent: distribution.metaDescriptionPresence)
 569                  DistributionIndicator(label: "Content", isPresent: distribution.contentPresence)
 570                  DistributionIndicator(label: "URL", isPresent: distribution.urlPresence)
 571              }
 572          }
 573          .padding(12)
 574          .background(Color(NSColor.textBackgroundColor))
 575          .cornerRadius(8)
 576      }
 577  }
 578  
 579  struct DistributionIndicator: View {
 580      let label: String
 581      let isPresent: Bool
 582      
 583      var body: some View {
 584          VStack(spacing: 4) {
 585              Circle()
 586                  .fill(isPresent ? Color.green : Color.secondary.opacity(0.3))
 587                  .frame(width: 8, height: 8)
 588              
 589              Text(label)
 590                  .font(.caption2)
 591                  .foregroundColor(.secondary)
 592          }
 593      }
 594  }
 595  
 596  struct TFIDFScoresView: View {
 597      let scores: [TFIDFScore]
 598      
 599      var body: some View {
 600          VStack(alignment: .leading, spacing: 16) {
 601              Text("TF-IDF Scores")
 602                  .font(.headline)
 603                  .fontWeight(.semibold)
 604              
 605              Text("Terms ranked by TF-IDF relevance score")
 606                  .font(.caption)
 607                  .foregroundColor(.secondary)
 608              
 609              LazyVStack(spacing: 8) {
 610                  ForEach(scores.prefix(10), id: \.id) { score in
 611                      TFIDFScoreRow(score: score)
 612                  }
 613              }
 614          }
 615      }
 616  }
 617  
 618  struct TFIDFScoreRow: View {
 619      let score: TFIDFScore
 620      
 621      var body: some View {
 622          HStack {
 623              VStack(alignment: .leading, spacing: 2) {
 624                  Text(score.term)
 625                      .font(.subheadline)
 626                      .fontWeight(.medium)
 627                  
 628                  Text("TF: \(String(format: "%.3f", score.termFrequency)) | IDF: \(String(format: "%.3f", score.inverseDocumentFrequency))")
 629                      .font(.caption)
 630                      .foregroundColor(.secondary)
 631              }
 632              
 633              Spacer()
 634              
 635              Text(String(format: "%.3f", score.tfIdf))
 636                  .font(.subheadline)
 637                  .fontWeight(.semibold)
 638                  .foregroundColor(.accentColor)
 639          }
 640          .padding(12)
 641          .background(Color(NSColor.textBackgroundColor))
 642          .cornerRadius(8)
 643      }
 644  }
 645  
 646  struct TechnicalTabContent: View {
 647      let analysis: SEOAnalysis
 648      
 649      var body: some View {
 650          ScrollView {
 651              VStack(spacing: 20) {
 652                  TechnicalSEOChecklist(technical: analysis.metrics.technicalSEO)
 653                  SemanticStructureView(semantic: analysis.metrics.semanticStructure)
 654                  // Links Analysis Summary
 655                  LinksAnalysisSummaryView(
 656                      internalLinks: analysis.metrics.internalLinks.links.map(\.url),
 657                      externalLinks: analysis.metrics.externalLinks.links.map(\.url)
 658                  )
 659              }
 660              .padding(20)
 661          }
 662      }
 663  }
 664  
 665  struct TechnicalSEOChecklist: View {
 666      let technical: TechnicalSEOAnalysis
 667      
 668      var body: some View {
 669          VStack(alignment: .leading, spacing: 16) {
 670              Text("Technical SEO Checklist")
 671                  .font(.headline)
 672                  .fontWeight(.semibold)
 673              
 674              VStack(spacing: 8) {
 675                  ChecklistItem(label: "Sitemap.xml", isPresent: technical.hasSitemap)
 676                  ChecklistItem(label: "Robots.txt", isPresent: technical.hasRobotsTxt)
 677                  ChecklistItem(label: "Canonical Tags", isPresent: technical.hasCanonicalTags)
 678                  ChecklistItem(label: "Open Graph Tags", isPresent: technical.hasOpenGraphTags)
 679                  ChecklistItem(label: "Twitter Card Tags", isPresent: technical.hasTwitterCardTags)
 680                  ChecklistItem(label: "Structured Data", isPresent: technical.hasStructuredData)
 681              }
 682          }
 683      }
 684  }
 685  
 686  struct ChecklistItem: View {
 687      let label: String
 688      let isPresent: Bool
 689      
 690      var body: some View {
 691          HStack {
 692              Image(systemName: isPresent ? "checkmark.circle.fill" : "xmark.circle.fill")
 693                  .foregroundColor(isPresent ? .green : .red)
 694              
 695              Text(label)
 696                  .font(.subheadline)
 697              
 698              Spacer()
 699          }
 700          .padding(.vertical, 4)
 701      }
 702  }
 703  
 704  struct SemanticStructureView: View {
 705      let semantic: SemanticStructureAnalysis
 706      
 707      var body: some View {
 708          VStack(alignment: .leading, spacing: 16) {
 709              Text("Semantic Structure")
 710                  .font(.headline)
 711                  .fontWeight(.semibold)
 712              
 713              LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 12) {
 714                  ForEach(semantic.semanticElements, id: \.id) { element in
 715                      SemanticElementCard(element: element)
 716                  }
 717              }
 718          }
 719      }
 720  }
 721  
 722  struct SemanticElementCard: View {
 723      let element: SemanticElement
 724      
 725      var body: some View {
 726          VStack(spacing: 8) {
 727              Text("<\(element.tag)>")
 728                  .font(.caption)
 729                  .fontWeight(.medium)
 730                  .foregroundColor(.accentColor)
 731              
 732              Text("\(element.count)")
 733                  .font(.title2)
 734                  .fontWeight(.bold)
 735              
 736              Circle()
 737                  .fill(element.isOptimal ? Color.green : Color.orange)
 738                  .frame(width: 8, height: 8)
 739          }
 740          .frame(maxWidth: .infinity)
 741          .padding(12)
 742          .background(Color(NSColor.textBackgroundColor))
 743          .cornerRadius(8)
 744      }
 745  }
 746  
 747  struct IssuesTabContent: View {
 748      let analysis: SEOAnalysis
 749      
 750      var body: some View {
 751          ScrollView {
 752              VStack(spacing: 20) {
 753                  IssuesListView(issues: analysis.issues)
 754                  SuggestionsListView(suggestions: analysis.suggestions)
 755              }
 756              .padding(20)
 757          }
 758      }
 759  }
 760  
 761  struct IssuesListView: View {
 762      let issues: [SEOIssue]
 763      
 764      var body: some View {
 765          VStack(alignment: .leading, spacing: 16) {
 766              Text("Issues Found (\(issues.count))")
 767                  .font(.headline)
 768                  .fontWeight(.semibold)
 769              
 770              if issues.isEmpty {
 771                  URLAnalysisEmptyStateView(
 772                      icon: "checkmark.circle",
 773                      title: "No issues found",
 774                      subtitle: "Great! This page doesn't have any SEO issues"
 775                  )
 776              } else {
 777                  LazyVStack(spacing: 12) {
 778                      ForEach(issues, id: \.id) { issue in
 779                          IssueCard(issue: issue)
 780                      }
 781                  }
 782              }
 783          }
 784      }
 785  }
 786  
 787  struct IssueCard: View {
 788      let issue: SEOIssue
 789      
 790      var body: some View {
 791          VStack(alignment: .leading, spacing: 12) {
 792              HStack {
 793                  Image(systemName: "exclamationmark.triangle.fill")
 794                      .foregroundColor(issue.severity.color)
 795                  
 796                  VStack(alignment: .leading, spacing: 4) {
 797                      Text(issue.title)
 798                          .font(.subheadline)
 799                          .fontWeight(.semibold)
 800                      
 801                      Text(issue.severity.rawValue.capitalized)
 802                          .font(.caption)
 803                          .padding(.horizontal, 8)
 804                          .padding(.vertical, 2)
 805                          .background(issue.severity.color.opacity(0.2))
 806                          .foregroundColor(issue.severity.color)
 807                          .cornerRadius(4)
 808                  }
 809                  
 810                  Spacer()
 811              }
 812              
 813              Text(issue.description)
 814                  .font(.subheadline)
 815                  .foregroundColor(.secondary)
 816              
 817              if let element = issue.element {
 818                  Text("Element: \(element)")
 819                      .font(.system(.caption, design: .monospaced))
 820                      .padding(8)
 821                      .background(Color.secondary.opacity(0.1))
 822                      .cornerRadius(4)
 823              }
 824              
 825              VStack(alignment: .leading, spacing: 4) {
 826                  Text("Recommendation:")
 827                      .font(.caption)
 828                      .fontWeight(.medium)
 829                  
 830                  Text(issue.recommendation)
 831                      .font(.caption)
 832                      .foregroundColor(.secondary)
 833              }
 834          }
 835          .padding(16)
 836          .background(Color(NSColor.textBackgroundColor))
 837          .cornerRadius(8)
 838          .overlay(
 839              RoundedRectangle(cornerRadius: 8)
 840                  .strokeBorder(issue.severity.color.opacity(0.3), lineWidth: 1)
 841          )
 842      }
 843  }
 844  
 845  struct SuggestionsListView: View {
 846      let suggestions: [SEOSuggestion]
 847      
 848      var body: some View {
 849          VStack(alignment: .leading, spacing: 16) {
 850              Text("Suggestions (\(suggestions.count))")
 851                  .font(.headline)
 852                  .fontWeight(.semibold)
 853              
 854              if suggestions.isEmpty {
 855                  URLAnalysisEmptyStateView(
 856                      icon: "lightbulb",
 857                      title: "No suggestions available",
 858                      subtitle: "This page is well optimized"
 859                  )
 860              } else {
 861                  LazyVStack(spacing: 12) {
 862                      ForEach(suggestions, id: \.id) { suggestion in
 863                          SuggestionCard(suggestion: suggestion)
 864                      }
 865                  }
 866              }
 867          }
 868      }
 869  }
 870  
 871  struct SuggestionCard: View {
 872      let suggestion: SEOSuggestion
 873      
 874      var body: some View {
 875          VStack(alignment: .leading, spacing: 12) {
 876              HStack {
 877                  Image(systemName: "lightbulb.fill")
 878                      .foregroundColor(.yellow)
 879                  
 880                  VStack(alignment: .leading, spacing: 4) {
 881                      Text(suggestion.title)
 882                          .font(.subheadline)
 883                          .fontWeight(.semibold)
 884                      
 885                      Text(suggestion.priority.rawValue.capitalized)
 886                          .font(.caption)
 887                          .padding(.horizontal, 8)
 888                          .padding(.vertical, 2)
 889                          .background(suggestion.priority.color.opacity(0.2))
 890                          .foregroundColor(suggestion.priority.color)
 891                          .cornerRadius(4)
 892                  }
 893                  
 894                  Spacer()
 895              }
 896              
 897              Text(suggestion.description)
 898                  .font(.subheadline)
 899                  .foregroundColor(.secondary)
 900              
 901              HStack(spacing: 16) {
 902                  VStack(alignment: .leading, spacing: 4) {
 903                      Text("Impact:")
 904                          .font(.caption)
 905                          .fontWeight(.medium)
 906                      
 907                      Text(suggestion.estimatedImpact)
 908                          .font(.caption)
 909                          .foregroundColor(.secondary)
 910                  }
 911                  
 912                  VStack(alignment: .leading, spacing: 4) {
 913                      Text("Difficulty:")
 914                          .font(.caption)
 915                          .fontWeight(.medium)
 916                      
 917                      Text(suggestion.implementationDifficulty)
 918                          .font(.caption)
 919                          .foregroundColor(.secondary)
 920                  }
 921              }
 922          }
 923          .padding(16)
 924          .background(Color(NSColor.textBackgroundColor))
 925          .cornerRadius(8)
 926          .overlay(
 927              RoundedRectangle(cornerRadius: 8)
 928                  .strokeBorder(Color.accentColor.opacity(0.3), lineWidth: 1)
 929          )
 930      }
 931  }
 932  
 933  extension SEOSuggestionPriority {
 934      var color: Color {
 935          switch self {
 936          case .critical: return .red
 937          case .high: return .orange
 938          case .medium: return .yellow
 939          case .low: return .blue
 940          }
 941      }
 942  }
 943  
 944  struct EmptyAnalysisView: View {
 945      var body: some View {
 946          VStack(spacing: 24) {
 947              Image(systemName: "magnifyingglass.circle")
 948                  .font(.system(size: 64))
 949                  .foregroundColor(.secondary)
 950              
 951              VStack(spacing: 8) {
 952                  Text("Ready to Analyze")
 953                      .font(.title2)
 954                      .fontWeight(.semibold)
 955                  
 956                  Text("Enter a URL above to start analyzing its SEO performance")
 957                      .font(.subheadline)
 958                      .foregroundColor(.secondary)
 959                      .multilineTextAlignment(.center)
 960              }
 961          }
 962          .frame(maxWidth: .infinity)
 963          .frame(height: 300)
 964          .background(Color(NSColor.controlBackgroundColor))
 965          .cornerRadius(12)
 966      }
 967  }
 968  
 969  // MARK: - Links Analysis Summary View
 970  
 971  struct LinksAnalysisSummaryView: View {
 972      let internalLinks: [String]
 973      let externalLinks: [String]
 974      
 975      var body: some View {
 976          VStack(alignment: .leading, spacing: 16) {
 977              Text("Links Analysis")
 978                  .font(.headline)
 979                  .fontWeight(.semibold)
 980              
 981              HStack(spacing: 20) {
 982                  internalLinksSection
 983                  externalLinksSection
 984              }
 985          }
 986          .padding(20)
 987          .background(Color(NSColor.controlBackgroundColor))
 988          .cornerRadius(12)
 989      }
 990      
 991      private var internalLinksSection: some View {
 992          VStack(alignment: .leading, spacing: 8) {
 993              HStack {
 994                  Image(systemName: "link")
 995                      .foregroundColor(.blue)
 996                  Text("Internal Links")
 997                      .font(.subheadline)
 998                      .fontWeight(.medium)
 999              }
1000              
1001              Text("\(internalLinks.count) links found")
1002                  .font(.caption)
1003                  .foregroundColor(.secondary)
1004              
1005              if !internalLinks.isEmpty {
1006                  ScrollView {
1007                      VStack(alignment: .leading, spacing: 4) {
1008                          ForEach(Array(internalLinks.prefix(5)), id: \.self) { link in
1009                              Text(link)
1010                                  .font(.caption)
1011                                  .foregroundColor(.secondary)
1012                                  .lineLimit(1)
1013                          }
1014                          
1015                          if internalLinks.count > 5 {
1016                              Text("... and \(internalLinks.count - 5) more")
1017                                  .font(.caption)
1018                                  .foregroundColor(.secondary)
1019                                  .italic()
1020                          }
1021                      }
1022                  }
1023                  .frame(maxHeight: 100)
1024              }
1025          }
1026          .frame(maxWidth: .infinity, alignment: .leading)
1027          .padding(12)
1028          .background(Color(NSColor.textBackgroundColor))
1029          .cornerRadius(8)
1030      }
1031      
1032      private var externalLinksSection: some View {
1033          VStack(alignment: .leading, spacing: 8) {
1034              HStack {
1035                  Image(systemName: "link.badge.plus")
1036                      .foregroundColor(.green)
1037                  Text("External Links")
1038                      .font(.subheadline)
1039                      .fontWeight(.medium)
1040              }
1041              
1042              Text("\(externalLinks.count) links found")
1043                  .font(.caption)
1044                  .foregroundColor(.secondary)
1045              
1046              if !externalLinks.isEmpty {
1047                  ScrollView {
1048                      VStack(alignment: .leading, spacing: 4) {
1049                          ForEach(Array(externalLinks.prefix(5)), id: \.self) { link in
1050                              Text(link)
1051                                  .font(.caption)
1052                                  .foregroundColor(.secondary)
1053                                  .lineLimit(1)
1054                          }
1055                          
1056                          if externalLinks.count > 5 {
1057                              Text("... and \(externalLinks.count - 5) more")
1058                                  .font(.caption)
1059                                  .foregroundColor(.secondary)
1060                                  .italic()
1061                          }
1062                      }
1063                  }
1064                  .frame(maxHeight: 100)
1065              }
1066          }
1067          .frame(maxWidth: .infinity, alignment: .leading)
1068          .padding(12)
1069          .background(Color(NSColor.textBackgroundColor))
1070          .cornerRadius(8)
1071      }
1072  }
1073  
1074  // MARK: - PageSpeed Insights Summary View
1075  
1076  struct PageSpeedInsightsSummaryView: View {
1077      let insights: PageSpeedInsights
1078      
1079      var body: some View {
1080          VStack(alignment: .leading, spacing: 16) {
1081              Text("PageSpeed Insights")
1082                  .font(.headline)
1083                  .fontWeight(.semibold)
1084              
1085              HStack(spacing: 20) {
1086                  // Performance Score
1087                  VStack(spacing: 8) {
1088                      Text("\(Int(insights.performanceScore))")
1089                          .font(.largeTitle)
1090                          .fontWeight(.bold)
1091                          .foregroundColor(scoreColor(insights.performanceScore))
1092                      
1093                      Text("Performance Score")
1094                          .font(.caption)
1095                          .foregroundColor(.secondary)
1096                  }
1097                  .frame(maxWidth: .infinity)
1098                  .padding(16)
1099                  .background(Color(NSColor.textBackgroundColor))
1100                  .cornerRadius(8)
1101                  
1102                  // Core Web Vitals
1103                  VStack(alignment: .leading, spacing: 12) {
1104                      Text("Core Web Vitals")
1105                          .font(.subheadline)
1106                          .fontWeight(.medium)
1107                      
1108                      VStack(alignment: .leading, spacing: 8) {
1109                          HStack {
1110                              Text("LCP:")
1111                                  .font(.caption)
1112                                  .fontWeight(.medium)
1113                              Spacer()
1114                              Text("\(String(format: "%.1f", insights.largestContentfulPaint ?? 0.0))s")
1115                                  .font(.caption)
1116                                  .foregroundColor(.secondary)
1117                          }
1118                          
1119                          HStack {
1120                              Text("FID:")
1121                                  .font(.caption)
1122                                  .fontWeight(.medium)
1123                              Spacer()
1124                              Text("\(String(format: "%.1f", insights.firstInputDelay ?? 0.0))ms")
1125                                  .font(.caption)
1126                                  .foregroundColor(.secondary)
1127                          }
1128                          
1129                          HStack {
1130                              Text("CLS:")
1131                                  .font(.caption)
1132                                  .fontWeight(.medium)
1133                              Spacer()
1134                              Text("\(String(format: "%.3f", insights.cumulativeLayoutShift ?? 0.0))")
1135                                  .font(.caption)
1136                                  .foregroundColor(.secondary)
1137                          }
1138                      }
1139                  }
1140                  .frame(maxWidth: .infinity, alignment: .leading)
1141                  .padding(16)
1142                  .background(Color(NSColor.textBackgroundColor))
1143                  .cornerRadius(8)
1144              }
1145              
1146              // Opportunities (if available)
1147              if !insights.opportunities.isEmpty {
1148                  VStack(alignment: .leading, spacing: 8) {
1149                      Text("Key Opportunities")
1150                          .font(.subheadline)
1151                          .fontWeight(.medium)
1152                      
1153                      ForEach(Array(insights.opportunities.prefix(3)), id: \.self) { opportunity in
1154                          HStack {
1155                              Image(systemName: "lightbulb.fill")
1156                                  .foregroundColor(.orange)
1157                                  .font(.caption)
1158                              
1159                              Text(opportunity)
1160                                  .font(.caption)
1161                                  .foregroundColor(.secondary)
1162                              
1163                              Spacer()
1164                          }
1165                      }
1166                      
1167                      if insights.opportunities.count > 3 {
1168                          Text("... and \(insights.opportunities.count - 3) more")
1169                              .font(.caption2)
1170                              .foregroundColor(.secondary)
1171                              .italic()
1172                      }
1173                  }
1174              }
1175          }
1176          .padding(20)
1177          .background(Color(NSColor.controlBackgroundColor))
1178          .cornerRadius(12)
1179      }
1180      
1181      private func scoreColor(_ score: Double) -> Color {
1182          switch score {
1183          case 90...: return .green
1184          case 50..<90: return .orange
1185          default: return .red
1186          }
1187      }
1188  }
1189  
1190  struct URLAnalysisEmptyStateView: View {
1191      let icon: String
1192      let title: String
1193      let subtitle: String
1194      
1195      var body: some View {
1196          VStack(spacing: 16) {
1197              Image(systemName: icon)
1198                  .font(.system(size: 48))
1199                  .foregroundColor(.secondary)
1200              
1201              Text(title)
1202                  .font(.title2)
1203                  .fontWeight(.semibold)
1204              
1205              Text(subtitle)
1206                  .font(.subheadline)
1207                  .foregroundColor(.secondary)
1208                  .multilineTextAlignment(.center)
1209          }
1210          .frame(maxWidth: .infinity, maxHeight: .infinity)
1211          .padding(40)
1212      }
1213  }
1214  
1215  #Preview {
1216      URLAnalysisView()
1217          .environmentObject(SEOAnalysisService())
1218          .frame(width: 1000, height: 800)
1219  }