JSAnalysisView.swift
1 // 2 // JSAnalysisView.swift 3 // RacerTracer 4 // 5 // Created by Alexander Kunau on 29.09.25. 6 // 7 8 import SwiftUI 9 import Charts 10 11 struct JSAnalysisView: View { 12 @StateObject private var analysisService = JSAnalysisService() 13 @EnvironmentObject private var settingsService: SettingsService 14 @State private var urlToAnalyze = "" 15 @State private var selectedFilter = "All Files" 16 @State private var selectedIssue = "All Issues" 17 18 var body: some View { 19 NavigationView { 20 VStack(spacing: 0) { 21 // Header Section 22 VStack(alignment: .leading, spacing: 16) { 23 Text("JavaScript Analysis") 24 .font(.title2) 25 .fontWeight(.semibold) 26 27 Text("Analyze JavaScript files for performance impact, loading behavior, execution time, and SEO optimization.") 28 .font(.subheadline) 29 .foregroundColor(.secondary) 30 } 31 .frame(maxWidth: .infinity, alignment: .leading) 32 .padding(24) 33 .background(Color(NSColor.controlBackgroundColor)) 34 35 // Analysis Input Section 36 VStack(spacing: 16) { 37 HStack { 38 TextField("Enter website URL to analyze JavaScript files", text: $urlToAnalyze) 39 .textFieldStyle(RoundedBorderTextFieldStyle()) 40 41 Button(action: { 42 Task { 43 await analysisService.analyzeJavaScript(url: urlToAnalyze) 44 } 45 }) { 46 if analysisService.isAnalyzing { 47 ProgressView() 48 .scaleEffect(0.8) 49 } else { 50 Text("Analyze") 51 } 52 } 53 .disabled(urlToAnalyze.isEmpty || analysisService.isAnalyzing) 54 .buttonStyle(.borderedProminent) 55 } 56 57 if analysisService.isAnalyzing { 58 ProgressView("Analyzing JavaScript performance...", value: analysisService.progress, total: 1.0) 59 .progressViewStyle(LinearProgressViewStyle()) 60 } 61 } 62 .padding(.horizontal, 24) 63 .padding(.bottom, 16) 64 65 // Content Area 66 ScrollView { 67 if let analysis = analysisService.currentAnalysis { 68 VStack(spacing: 24) { 69 // Overview Cards 70 JSOverviewCardsSection(analysis: analysis) 71 72 // Filters 73 JSFiltersSection( 74 selectedFilter: $selectedFilter, 75 selectedIssue: $selectedIssue 76 ) 77 78 // Performance Impact Charts 79 JSPerformanceChartsSection(analysis: analysis) 80 81 // Critical JS Issues Section 82 if !analysis.criticalIssues.isEmpty { 83 CriticalJSIssuesSection(issues: analysis.criticalIssues) 84 } 85 86 // Large JavaScript Files Section 87 if !analysis.largeJSFiles.isEmpty { 88 LargeJSFilesSection(files: analysis.largeJSFiles) 89 } 90 91 // Render Blocking JS Section 92 if !analysis.renderBlockingJS.isEmpty { 93 RenderBlockingJSSection(files: analysis.renderBlockingJS) 94 } 95 96 // Third-party Scripts Section 97 if !analysis.thirdPartyScripts.isEmpty { 98 ThirdPartyScriptsSection(scripts: analysis.thirdPartyScripts) 99 } 100 101 // JavaScript Files List 102 JSFilesListSection( 103 files: filteredJSFiles(analysis.jsFiles), 104 selectedFilter: selectedFilter, 105 selectedIssue: selectedIssue 106 ) 107 108 // Performance & SEO Recommendations 109 JSRecommendationsSection(analysis: analysis) 110 } 111 .padding(24) 112 } else if !analysisService.isAnalyzing { 113 JSEmptyStateView() 114 } 115 } 116 } 117 } 118 .navigationTitle("JavaScript Analysis") 119 } 120 121 private func filteredJSFiles(_ files: [JSFile]) -> [JSFile] { 122 var filtered = files 123 124 switch selectedFilter { 125 case "External Only": 126 filtered = filtered.filter { $0.isExternal } 127 case "Internal Only": 128 filtered = filtered.filter { !$0.isExternal } 129 case "Large Files": 130 filtered = filtered.filter { $0.fileSize > 100000 } // > 100KB 131 case "Render Blocking": 132 filtered = filtered.filter { $0.isRenderBlocking } 133 case "Third-party": 134 filtered = filtered.filter { $0.isThirdParty } 135 default: 136 break 137 } 138 139 switch selectedIssue { 140 case "Slow Execution": 141 filtered = filtered.filter { $0.executionTime > 100 } // > 100ms 142 case "Large Size": 143 filtered = filtered.filter { $0.fileSize > 100000 } 144 case "Blocking Render": 145 filtered = filtered.filter { $0.isRenderBlocking } 146 case "Memory Issues": 147 filtered = filtered.filter { $0.memoryUsage > 1000000 } // > 1MB 148 case "No Issues": 149 filtered = filtered.filter { $0.fileSize <= 100000 && $0.executionTime <= 100 && !$0.isRenderBlocking } 150 default: 151 break 152 } 153 154 return filtered 155 } 156 } 157 158 // MARK: - JS Overview Cards Section 159 160 struct JSOverviewCardsSection: View { 161 let analysis: JSAnalysis 162 163 var body: some View { 164 VStack(alignment: .leading, spacing: 16) { 165 Text("JavaScript Performance Overview") 166 .font(.headline) 167 .fontWeight(.semibold) 168 169 LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 16) { 170 OverviewCard( 171 title: "Total JS Files", 172 value: "\(analysis.totalJSFiles)", 173 subtitle: "Found on website", 174 color: .blue, 175 icon: "curlybraces" 176 ) 177 178 OverviewCard( 179 title: "Total JS Size", 180 value: formatFileSize(analysis.totalJSSize), 181 subtitle: "Combined size", 182 color: analysis.totalJSSize < 200000 ? .green : analysis.totalJSSize < 500000 ? .orange : .red, 183 icon: "tray.full" 184 ) 185 186 OverviewCard( 187 title: "Execution Time", 188 value: "\(Int(analysis.totalExecutionTime))ms", 189 subtitle: "Total execution", 190 color: analysis.totalExecutionTime < 500 ? .green : analysis.totalExecutionTime < 1000 ? .orange : .red, 191 icon: "clock" 192 ) 193 194 OverviewCard( 195 title: "Render Blocking", 196 value: "\(analysis.renderBlockingFiles)", 197 subtitle: "Files blocking render", 198 color: analysis.renderBlockingFiles == 0 ? .green : analysis.renderBlockingFiles < 3 ? .orange : .red, 199 icon: "exclamationmark.triangle" 200 ) 201 } 202 203 // Additional metrics row 204 LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 16) { 205 OverviewCard( 206 title: "Third-party Scripts", 207 value: "\(analysis.thirdPartyScripts.count)", 208 subtitle: "External scripts", 209 color: .purple, 210 icon: "globe" 211 ) 212 213 OverviewCard( 214 title: "Memory Usage", 215 value: formatMemorySize(analysis.totalMemoryUsage), 216 subtitle: "Heap memory", 217 color: analysis.totalMemoryUsage < 5000000 ? .green : analysis.totalMemoryUsage < 10000000 ? .orange : .red, 218 icon: "memorychip" 219 ) 220 221 OverviewCard( 222 title: "Async Scripts", 223 value: "\(analysis.asyncScripts)", 224 subtitle: "Non-blocking", 225 color: .green, 226 icon: "arrow.clockwise" 227 ) 228 229 OverviewCard( 230 title: "Performance Score", 231 value: String(format: "%.0f%%", analysis.performanceScore * 100), 232 subtitle: "JS optimization", 233 color: analysis.performanceScore > 0.8 ? .green : analysis.performanceScore > 0.6 ? .orange : .red, 234 icon: "checkmark.shield" 235 ) 236 } 237 } 238 } 239 240 private func formatFileSize(_ bytes: Int) -> String { 241 let formatter = ByteCountFormatter() 242 formatter.allowedUnits = [.useKB, .useMB] 243 formatter.countStyle = .file 244 return formatter.string(fromByteCount: Int64(bytes)) 245 } 246 247 private func formatMemorySize(_ bytes: Int) -> String { 248 let formatter = ByteCountFormatter() 249 formatter.allowedUnits = [.useMB] 250 formatter.countStyle = .memory 251 return formatter.string(fromByteCount: Int64(bytes)) 252 } 253 } 254 255 // MARK: - JS Performance Charts Section 256 257 struct JSPerformanceChartsSection: View { 258 let analysis: JSAnalysis 259 260 var body: some View { 261 VStack(alignment: .leading, spacing: 20) { 262 Text("Performance Analysis Charts") 263 .font(.headline) 264 .fontWeight(.semibold) 265 266 HStack(spacing: 20) { 267 // Execution Time Distribution Chart 268 VStack(alignment: .leading, spacing: 12) { 269 Text("Execution Time Distribution") 270 .font(.subheadline) 271 .fontWeight(.medium) 272 273 Chart(analysis.executionTimeDistribution) { item in 274 BarMark( 275 x: .value("Time Range", item.range), 276 y: .value("Files", item.count) 277 ) 278 .foregroundStyle(.red.gradient) 279 .cornerRadius(4) 280 } 281 .frame(height: 180) 282 .chartXAxis { 283 AxisMarks { _ in 284 AxisValueLabel() 285 .font(.caption) 286 } 287 } 288 } 289 .frame(maxWidth: .infinity) 290 291 // File Size vs Execution Time Chart 292 VStack(alignment: .leading, spacing: 12) { 293 Text("Size vs Execution Time") 294 .font(.subheadline) 295 .fontWeight(.medium) 296 297 Chart(analysis.jsFiles.prefix(15)) { file in 298 PointMark( 299 x: .value("File Size (KB)", file.fileSize / 1000), 300 y: .value("Execution Time (ms)", file.executionTime) 301 ) 302 .foregroundStyle(file.isRenderBlocking ? .red : .blue) 303 .symbolSize(file.isRenderBlocking ? 50 : 30) 304 } 305 .frame(height: 180) 306 } 307 .frame(maxWidth: .infinity) 308 } 309 310 // Loading Strategy Distribution 311 VStack(alignment: .leading, spacing: 12) { 312 Text("Loading Strategy Distribution") 313 .font(.subheadline) 314 .fontWeight(.medium) 315 316 Chart(analysis.loadingStrategyDistribution) { item in 317 SectorMark( 318 angle: .value("Count", item.count), 319 innerRadius: .ratio(0.5), 320 angularInset: 2 321 ) 322 .foregroundStyle(item.strategy.color.gradient) 323 .cornerRadius(3) 324 } 325 .frame(height: 200) 326 } 327 } 328 .padding(20) 329 .background(Color(NSColor.controlBackgroundColor)) 330 .cornerRadius(12) 331 } 332 } 333 334 // MARK: - Critical JS Issues Section 335 336 struct CriticalJSIssuesSection: View { 337 let issues: [JSIssue] 338 339 var body: some View { 340 VStack(alignment: .leading, spacing: 16) { 341 HStack { 342 Image(systemName: "exclamationmark.triangle.fill") 343 .foregroundColor(.red) 344 Text("Critical JavaScript Issues") 345 .font(.headline) 346 .fontWeight(.semibold) 347 348 Spacer() 349 350 Text("\(issues.count) issues") 351 .font(.subheadline) 352 .foregroundColor(.red) 353 .padding(.horizontal, 12) 354 .padding(.vertical, 4) 355 .background(.red.opacity(0.1)) 356 .cornerRadius(8) 357 } 358 359 LazyVStack(spacing: 8) { 360 ForEach(issues.prefix(10)) { issue in 361 CriticalJSIssueRow(issue: issue) 362 } 363 364 if issues.count > 10 { 365 Text("... and \(issues.count - 10) more critical issues") 366 .font(.caption) 367 .foregroundColor(.secondary) 368 .padding(.top, 8) 369 } 370 } 371 } 372 .padding(20) 373 .background(Color(NSColor.controlBackgroundColor)) 374 .cornerRadius(12) 375 } 376 } 377 378 struct CriticalJSIssueRow: View { 379 let issue: JSIssue 380 381 var body: some View { 382 HStack { 383 Image(systemName: issue.severity.icon) 384 .foregroundColor(issue.severity.color) 385 .font(.title2) 386 387 VStack(alignment: .leading, spacing: 4) { 388 Text(issue.title) 389 .font(.subheadline) 390 .fontWeight(.medium) 391 392 Text(issue.description) 393 .font(.caption) 394 .foregroundColor(.secondary) 395 396 if !issue.filename.isEmpty { 397 Text("File: \(issue.filename)") 398 .font(.caption) 399 .foregroundColor(.blue) 400 } 401 } 402 403 Spacer() 404 405 VStack(alignment: .trailing, spacing: 4) { 406 Text(issue.severity.displayName) 407 .font(.caption) 408 .fontWeight(.medium) 409 .foregroundColor(.white) 410 .padding(.horizontal, 8) 411 .padding(.vertical, 4) 412 .background(issue.severity.color) 413 .cornerRadius(6) 414 415 if let impact = issue.performanceImpact { 416 Text("Impact: \(impact)ms") 417 .font(.caption) 418 .foregroundColor(.secondary) 419 } 420 } 421 } 422 .padding(12) 423 .background(.red.opacity(0.05)) 424 .cornerRadius(8) 425 } 426 } 427 428 // MARK: - Large JS Files Section 429 430 struct LargeJSFilesSection: View { 431 let files: [JSFile] 432 433 var body: some View { 434 VStack(alignment: .leading, spacing: 16) { 435 HStack { 436 Image(systemName: "exclamationmark.circle.fill") 437 .foregroundColor(.orange) 438 Text("Large JavaScript Files") 439 .font(.headline) 440 .fontWeight(.semibold) 441 442 Spacer() 443 444 Text("\(files.count) files") 445 .font(.subheadline) 446 .foregroundColor(.orange) 447 .padding(.horizontal, 12) 448 .padding(.vertical, 4) 449 .background(.orange.opacity(0.1)) 450 .cornerRadius(8) 451 } 452 453 LazyVStack(spacing: 8) { 454 ForEach(files.prefix(10)) { file in 455 LargeJSFileRow(file: file) 456 } 457 458 if files.count > 10 { 459 Text("... and \(files.count - 10) more large files") 460 .font(.caption) 461 .foregroundColor(.secondary) 462 .padding(.top, 8) 463 } 464 } 465 } 466 .padding(20) 467 .background(Color(NSColor.controlBackgroundColor)) 468 .cornerRadius(12) 469 } 470 } 471 472 struct LargeJSFileRow: View { 473 let file: JSFile 474 475 var body: some View { 476 HStack { 477 Image(systemName: "curlybraces") 478 .foregroundColor(.blue) 479 .font(.title2) 480 481 VStack(alignment: .leading, spacing: 4) { 482 Text(file.filename) 483 .font(.subheadline) 484 .fontWeight(.medium) 485 .lineLimit(1) 486 487 Text(file.url) 488 .font(.caption) 489 .foregroundColor(.secondary) 490 .lineLimit(1) 491 492 HStack { 493 if file.isRenderBlocking { 494 Text("Render Blocking") 495 .font(.caption) 496 .fontWeight(.medium) 497 .foregroundColor(.white) 498 .padding(.horizontal, 6) 499 .padding(.vertical, 2) 500 .background(.red) 501 .cornerRadius(4) 502 } 503 504 if file.isThirdParty { 505 Text("Third-party") 506 .font(.caption) 507 .fontWeight(.medium) 508 .foregroundColor(.white) 509 .padding(.horizontal, 6) 510 .padding(.vertical, 2) 511 .background(.purple) 512 .cornerRadius(4) 513 } 514 515 if file.isAsync { 516 Text("Async") 517 .font(.caption) 518 .fontWeight(.medium) 519 .foregroundColor(.white) 520 .padding(.horizontal, 6) 521 .padding(.vertical, 2) 522 .background(.green) 523 .cornerRadius(4) 524 } 525 } 526 } 527 528 Spacer() 529 530 VStack(alignment: .trailing, spacing: 4) { 531 Text(formatFileSize(file.fileSize)) 532 .font(.subheadline) 533 .fontWeight(.bold) 534 .foregroundColor(.orange) 535 536 Text("\(Int(file.executionTime))ms") 537 .font(.caption) 538 .foregroundColor(.secondary) 539 540 Text(formatMemorySize(file.memoryUsage)) 541 .font(.caption) 542 .foregroundColor(.secondary) 543 } 544 } 545 .padding(12) 546 .background(.orange.opacity(0.05)) 547 .cornerRadius(8) 548 } 549 550 private func formatFileSize(_ bytes: Int) -> String { 551 let formatter = ByteCountFormatter() 552 formatter.allowedUnits = [.useKB, .useMB] 553 formatter.countStyle = .file 554 return formatter.string(fromByteCount: Int64(bytes)) 555 } 556 557 private func formatMemorySize(_ bytes: Int) -> String { 558 let formatter = ByteCountFormatter() 559 formatter.allowedUnits = [.useKB, .useMB] 560 formatter.countStyle = .memory 561 return formatter.string(fromByteCount: Int64(bytes)) 562 } 563 } 564 565 // MARK: - Render Blocking JS Section 566 567 struct RenderBlockingJSSection: View { 568 let files: [JSFile] 569 570 var body: some View { 571 VStack(alignment: .leading, spacing: 16) { 572 HStack { 573 Image(systemName: "exclamationmark.triangle.fill") 574 .foregroundColor(.red) 575 Text("Render Blocking JavaScript") 576 .font(.headline) 577 .fontWeight(.semibold) 578 579 Spacer() 580 581 Text("\(files.count) files") 582 .font(.subheadline) 583 .foregroundColor(.red) 584 .padding(.horizontal, 12) 585 .padding(.vertical, 4) 586 .background(.red.opacity(0.1)) 587 .cornerRadius(8) 588 } 589 590 Text("These scripts block the rendering of the page and should be optimized or moved to load asynchronously.") 591 .font(.caption) 592 .foregroundColor(.secondary) 593 594 LazyVStack(spacing: 8) { 595 ForEach(files.prefix(10)) { file in 596 RenderBlockingJSRow(file: file) 597 } 598 599 if files.count > 10 { 600 Text("... and \(files.count - 10) more render blocking files") 601 .font(.caption) 602 .foregroundColor(.secondary) 603 .padding(.top, 8) 604 } 605 } 606 } 607 .padding(20) 608 .background(Color(NSColor.controlBackgroundColor)) 609 .cornerRadius(12) 610 } 611 } 612 613 struct RenderBlockingJSRow: View { 614 let file: JSFile 615 616 var body: some View { 617 HStack { 618 Image(systemName: "exclamationmark.triangle.fill") 619 .foregroundColor(.red) 620 621 VStack(alignment: .leading, spacing: 4) { 622 Text(file.filename) 623 .font(.subheadline) 624 .fontWeight(.medium) 625 .lineLimit(1) 626 627 Text(file.url) 628 .font(.caption) 629 .foregroundColor(.secondary) 630 .lineLimit(1) 631 632 Text("Blocking render for \(Int(file.executionTime))ms") 633 .font(.caption) 634 .foregroundColor(.red) 635 } 636 637 Spacer() 638 639 VStack(alignment: .trailing, spacing: 4) { 640 Text("BLOCKING") 641 .font(.caption) 642 .fontWeight(.bold) 643 .foregroundColor(.white) 644 .padding(.horizontal, 8) 645 .padding(.vertical, 4) 646 .background(.red) 647 .cornerRadius(6) 648 649 Text(formatFileSize(file.fileSize)) 650 .font(.caption) 651 .foregroundColor(.secondary) 652 } 653 } 654 .padding(12) 655 .background(.red.opacity(0.05)) 656 .cornerRadius(8) 657 } 658 659 private func formatFileSize(_ bytes: Int) -> String { 660 let formatter = ByteCountFormatter() 661 formatter.allowedUnits = [.useKB, .useMB] 662 formatter.countStyle = .file 663 return formatter.string(fromByteCount: Int64(bytes)) 664 } 665 } 666 667 // MARK: - Third-party Scripts Section 668 669 struct ThirdPartyScriptsSection: View { 670 let scripts: [ThirdPartyScript] 671 672 var body: some View { 673 VStack(alignment: .leading, spacing: 16) { 674 HStack { 675 Image(systemName: "globe") 676 .foregroundColor(.purple) 677 Text("Third-party Scripts") 678 .font(.headline) 679 .fontWeight(.semibold) 680 681 Spacer() 682 683 Text("\(scripts.count) scripts") 684 .font(.subheadline) 685 .foregroundColor(.purple) 686 .padding(.horizontal, 12) 687 .padding(.vertical, 4) 688 .background(.purple.opacity(0.1)) 689 .cornerRadius(8) 690 } 691 692 LazyVStack(spacing: 8) { 693 ForEach(scripts.prefix(10)) { script in 694 ThirdPartyScriptRow(script: script) 695 } 696 697 if scripts.count > 10 { 698 Text("... and \(scripts.count - 10) more third-party scripts") 699 .font(.caption) 700 .foregroundColor(.secondary) 701 .padding(.top, 8) 702 } 703 } 704 } 705 .padding(20) 706 .background(Color(NSColor.controlBackgroundColor)) 707 .cornerRadius(12) 708 } 709 } 710 711 struct ThirdPartyScriptRow: View { 712 let script: ThirdPartyScript 713 714 var body: some View { 715 HStack { 716 VStack(alignment: .leading, spacing: 4) { 717 Text(script.domain) 718 .font(.subheadline) 719 .fontWeight(.medium) 720 721 Text(script.purpose) 722 .font(.caption) 723 .foregroundColor(.secondary) 724 725 Text("\(script.fileCount) files") 726 .font(.caption) 727 .foregroundColor(.blue) 728 } 729 730 Spacer() 731 732 VStack(alignment: .trailing, spacing: 4) { 733 Text(formatFileSize(script.totalSize)) 734 .font(.caption) 735 .fontWeight(.medium) 736 .foregroundColor(.purple) 737 738 Text("\(Int(script.totalExecutionTime))ms") 739 .font(.caption) 740 .foregroundColor(.secondary) 741 742 Text(script.impactLevel.rawValue) 743 .font(.caption) 744 .fontWeight(.medium) 745 .foregroundColor(.white) 746 .padding(.horizontal, 6) 747 .padding(.vertical, 2) 748 .background(script.impactLevel.color) 749 .cornerRadius(4) 750 } 751 } 752 .padding(10) 753 .background(Color(NSColor.textBackgroundColor)) 754 .cornerRadius(8) 755 } 756 757 private func formatFileSize(_ bytes: Int) -> String { 758 let formatter = ByteCountFormatter() 759 formatter.allowedUnits = [.useKB, .useMB] 760 formatter.countStyle = .file 761 return formatter.string(fromByteCount: Int64(bytes)) 762 } 763 } 764 765 // MARK: - Supporting Views 766 767 struct JSFiltersSection: View { 768 @Binding var selectedFilter: String 769 @Binding var selectedIssue: String 770 771 var body: some View { 772 VStack(alignment: .leading, spacing: 16) { 773 Text("Filters") 774 .font(.headline) 775 .fontWeight(.semibold) 776 777 HStack(spacing: 20) { 778 VStack(alignment: .leading, spacing: 8) { 779 Text("File Type") 780 .font(.subheadline) 781 .fontWeight(.medium) 782 783 Picker("Filter", selection: $selectedFilter) { 784 Text("All Files").tag("All Files") 785 Text("External Only").tag("External Only") 786 Text("Internal Only").tag("Internal Only") 787 Text("Large Files").tag("Large Files") 788 Text("Render Blocking").tag("Render Blocking") 789 Text("Third-party").tag("Third-party") 790 } 791 .pickerStyle(MenuPickerStyle()) 792 } 793 794 VStack(alignment: .leading, spacing: 8) { 795 Text("Issues") 796 .font(.subheadline) 797 .fontWeight(.medium) 798 799 Picker("Issues", selection: $selectedIssue) { 800 Text("All Issues").tag("All Issues") 801 Text("Slow Execution").tag("Slow Execution") 802 Text("Large Size").tag("Large Size") 803 Text("Blocking Render").tag("Blocking Render") 804 Text("Memory Issues").tag("Memory Issues") 805 Text("No Issues").tag("No Issues") 806 } 807 .pickerStyle(MenuPickerStyle()) 808 } 809 810 Spacer() 811 } 812 } 813 .padding(20) 814 .background(Color(NSColor.controlBackgroundColor)) 815 .cornerRadius(12) 816 } 817 } 818 819 struct JSFilesListSection: View { 820 let files: [JSFile] 821 let selectedFilter: String 822 let selectedIssue: String 823 824 var body: some View { 825 VStack(alignment: .leading, spacing: 16) { 826 HStack { 827 Text("JavaScript Files") 828 .font(.headline) 829 .fontWeight(.semibold) 830 831 Spacer() 832 833 Text("\(files.count) files") 834 .font(.subheadline) 835 .foregroundColor(.secondary) 836 } 837 838 LazyVStack(spacing: 8) { 839 ForEach(files.prefix(50)) { file in 840 JSFileRow(file: file) 841 } 842 843 if files.count > 50 { 844 Text("... and \(files.count - 50) more files") 845 .font(.caption) 846 .foregroundColor(.secondary) 847 .padding(.top, 8) 848 } 849 } 850 } 851 .padding(20) 852 .background(Color(NSColor.controlBackgroundColor)) 853 .cornerRadius(12) 854 } 855 } 856 857 struct JSFileRow: View { 858 let file: JSFile 859 860 var body: some View { 861 HStack { 862 Image(systemName: "curlybraces") 863 .foregroundColor(.blue) 864 .font(.title3) 865 866 VStack(alignment: .leading, spacing: 4) { 867 Text(file.filename) 868 .font(.subheadline) 869 .fontWeight(.medium) 870 .lineLimit(1) 871 872 Text(file.url) 873 .font(.caption) 874 .foregroundColor(.secondary) 875 .lineLimit(1) 876 877 HStack { 878 if file.isRenderBlocking { 879 Text("Blocking") 880 .font(.caption) 881 .foregroundColor(.white) 882 .padding(.horizontal, 4) 883 .padding(.vertical, 1) 884 .background(.red) 885 .cornerRadius(3) 886 } 887 888 if file.isThirdParty { 889 Text("3rd Party") 890 .font(.caption) 891 .foregroundColor(.white) 892 .padding(.horizontal, 4) 893 .padding(.vertical, 1) 894 .background(.purple) 895 .cornerRadius(3) 896 } 897 898 if file.isAsync { 899 Text("Async") 900 .font(.caption) 901 .foregroundColor(.white) 902 .padding(.horizontal, 4) 903 .padding(.vertical, 1) 904 .background(.green) 905 .cornerRadius(3) 906 } 907 } 908 } 909 910 Spacer() 911 912 VStack(alignment: .trailing, spacing: 4) { 913 Text(formatFileSize(file.fileSize)) 914 .font(.caption) 915 .fontWeight(.medium) 916 .foregroundColor(file.fileSize > 100000 ? .orange : .secondary) 917 918 Text("\(Int(file.executionTime))ms") 919 .font(.caption) 920 .foregroundColor(file.executionTime > 100 ? .red : .secondary) 921 922 Text(formatMemorySize(file.memoryUsage)) 923 .font(.caption) 924 .foregroundColor(.secondary) 925 } 926 } 927 .padding(10) 928 .background(Color(NSColor.textBackgroundColor)) 929 .cornerRadius(8) 930 } 931 932 private func formatFileSize(_ bytes: Int) -> String { 933 let formatter = ByteCountFormatter() 934 formatter.allowedUnits = [.useKB, .useMB] 935 formatter.countStyle = .file 936 return formatter.string(fromByteCount: Int64(bytes)) 937 } 938 939 private func formatMemorySize(_ bytes: Int) -> String { 940 let formatter = ByteCountFormatter() 941 formatter.allowedUnits = [.useKB, .useMB] 942 formatter.countStyle = .memory 943 return formatter.string(fromByteCount: Int64(bytes)) 944 } 945 } 946 947 struct JSRecommendationsSection: View { 948 let analysis: JSAnalysis 949 950 var body: some View { 951 VStack(alignment: .leading, spacing: 16) { 952 Text("JavaScript Performance Recommendations") 953 .font(.headline) 954 .fontWeight(.semibold) 955 956 LazyVStack(spacing: 12) { 957 ForEach(analysis.recommendations, id: \.title) { recommendation in 958 CommonSEORecommendationCard(recommendation: recommendation) 959 } 960 } 961 } 962 .padding(20) 963 .background(Color(NSColor.controlBackgroundColor)) 964 .cornerRadius(12) 965 } 966 } 967 968 struct JSEmptyStateView: View { 969 var body: some View { 970 VStack(spacing: 16) { 971 Image(systemName: "curlybraces") 972 .font(.system(size: 64)) 973 .foregroundColor(.secondary) 974 975 Text("No JavaScript Analysis Yet") 976 .font(.title2) 977 .fontWeight(.semibold) 978 979 Text("Enter a website URL above to start analyzing JavaScript files for performance optimization and SEO impact.") 980 .font(.subheadline) 981 .foregroundColor(.secondary) 982 .multilineTextAlignment(.center) 983 .padding(.horizontal, 40) 984 } 985 .frame(maxWidth: .infinity, maxHeight: .infinity) 986 } 987 } 988 989 #Preview { 990 JSAnalysisView() 991 .environmentObject(SettingsService.shared) 992 }