Просмотр исходного кода

Add dashboard job keyword search and fix listings scroll alignment

Wire search submission to filter mock listings by whitespace-separated
tokens (case-insensitive). Show empty-state copy when nothing matches,
return full catalog for empty query, and handle Return in the keywords
field. Use a flipped document view so short result lists sit under the
search bar without a large gap. Tweak mock job description wording.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 недель назад: 3
Родитель
Сommit
9786ce1919

+ 2 - 2
App for Indeed/Models/DashboardModels.swift

@@ -40,7 +40,7 @@ final class MockDashboardDataProvider: DashboardDataProviding {
40 40
             jobListings: [
41 41
                 JobListing(
42 42
                     title: "Senior iOS Engineer",
43
-                    description: "Build polished native experiences in Swift and SwiftUI. Remote-friendly team, strong product focus, and mentorship for mid-level engineers."
43
+                    description: "Build polished native software in Swift and SwiftUI. Remote-friendly team, strong product focus, and mentorship for mid-level engineers."
44 44
                 ),
45 45
                 JobListing(
46 46
                     title: "Product Designer",
@@ -48,7 +48,7 @@ final class MockDashboardDataProvider: DashboardDataProviding {
48 48
                 ),
49 49
                 JobListing(
50 50
                     title: "Machine Learning Engineer",
51
-                    description: "Improve search and recommendations using large-scale data. Python, PyTorch, and production ML pipelines; research-to-ship mindset."
51
+                    description: "Improve search and recommendations using large-scale data. Python, PyTorch, and production software for ML pipelines; research-to-ship mindset."
52 52
                 ),
53 53
                 JobListing(
54 54
                     title: "Technical Recruiter",

+ 51 - 5
App for Indeed/Views/DashboardView.swift

@@ -50,11 +50,14 @@ final class DashboardView: NSView, NSTextFieldDelegate {
50 50
     private let findJobsCTAChrome = NSView()
51 51
     private var findJobsCTAGradientLayer: CAGradientLayer?
52 52
     private let jobListingsScrollView = NSScrollView()
53
-    private let jobListingsContainer = NSView()
53
+    /// Flipped so short result lists stay visually under the search bar instead of leaving a gap above the cards.
54
+    private let jobListingsContainer = JobListingsDocumentView()
54 55
     private let jobListingsStack = NSStackView()
55 56
 
56 57
     private var currentSidebarItems: [SidebarItem] = []
57 58
     private var selectedSidebarIndex: Int = 0
59
+    /// Full list from `DashboardData`; results are shown after the user runs a search.
60
+    private var catalogJobListings: [JobListing] = []
58 61
 
59 62
     override init(frame frameRect: NSRect) {
60 63
         super.init(frame: frameRect)
@@ -82,7 +85,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
82 85
             selectedSidebarIndex = max(0, currentSidebarItems.count - 1)
83 86
         }
84 87
         configureSidebar()
85
-        configureJobListings(data.jobListings)
88
+        catalogJobListings = data.jobListings
89
+        configureJobListings([], noResultsForQuery: nil)
86 90
     }
87 91
 
88 92
     private func setupLayout() {
@@ -157,7 +161,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
157 161
 
158 162
         let listingsTopSpacer = NSView()
159 163
         listingsTopSpacer.translatesAutoresizingMaskIntoConstraints = false
160
-        listingsTopSpacer.heightAnchor.constraint(equalToConstant: 28).isActive = true
164
+        listingsTopSpacer.heightAnchor.constraint(equalToConstant: 12).isActive = true
161 165
 
162 166
         jobListingsContainer.translatesAutoresizingMaskIntoConstraints = false
163 167
         jobListingsStack.orientation = .vertical
@@ -248,11 +252,22 @@ final class DashboardView: NSView, NSTextFieldDelegate {
248 252
         }
249 253
     }
250 254
 
251
-    private func configureJobListings(_ jobs: [JobListing]) {
255
+    private func configureJobListings(_ jobs: [JobListing], noResultsForQuery: String?) {
252 256
         jobListingsStack.arrangedSubviews.forEach {
253 257
             jobListingsStack.removeArrangedSubview($0)
254 258
             $0.removeFromSuperview()
255 259
         }
260
+        if jobs.isEmpty, let query = noResultsForQuery, !query.isEmpty {
261
+            let empty = NSTextField(wrappingLabelWithString: "No jobs match “\(query)”. Try different keywords or browse the full list with an empty search.")
262
+            empty.font = .systemFont(ofSize: 14, weight: .regular)
263
+            empty.textColor = Theme.secondaryText
264
+            empty.alignment = .center
265
+            empty.maximumNumberOfLines = 0
266
+            empty.translatesAutoresizingMaskIntoConstraints = false
267
+            jobListingsStack.addArrangedSubview(empty)
268
+            empty.widthAnchor.constraint(equalTo: jobListingsStack.widthAnchor).isActive = true
269
+            return
270
+        }
256 271
         for job in jobs {
257 272
             let card = makeJobListingCard(job)
258 273
             jobListingsStack.addArrangedSubview(card)
@@ -483,7 +498,25 @@ final class DashboardView: NSView, NSTextFieldDelegate {
483 498
     }
484 499
 
485 500
     @objc private func didSubmitSearch() {
486
-        // Hook up search submission here when wiring up real data.
501
+        let query = jobKeywordsField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
502
+        let results = jobsMatchingSearch(query: query, in: catalogJobListings)
503
+        let noResultsMessage: String? = (!results.isEmpty || query.isEmpty) ? nil : query
504
+        configureJobListings(results, noResultsForQuery: noResultsMessage)
505
+        window?.makeFirstResponder(nil)
506
+    }
507
+
508
+    /// Each whitespace-separated token must appear in the title or description (case-insensitive). Empty query returns the full catalog.
509
+    private func jobsMatchingSearch(query: String, in jobs: [JobListing]) -> [JobListing] {
510
+        let tokens = query
511
+            .lowercased()
512
+            .split(whereSeparator: { $0.isWhitespace })
513
+            .map(String.init)
514
+            .filter { !$0.isEmpty }
515
+        guard !tokens.isEmpty else { return jobs }
516
+        return jobs.filter { job in
517
+            let haystack = "\(job.title) \(job.description)".lowercased()
518
+            return tokens.allSatisfy { haystack.contains($0) }
519
+        }
487 520
     }
488 521
 
489 522
     func controlTextDidBeginEditing(_ obj: Notification) {
@@ -494,6 +527,14 @@ final class DashboardView: NSView, NSTextFieldDelegate {
494 527
         applySearchFieldInsertionPoint(obj.object)
495 528
     }
496 529
 
530
+    func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
531
+        guard control === jobKeywordsField, commandSelector == #selector(NSResponder.insertNewline(_:)) else {
532
+            return false
533
+        }
534
+        didSubmitSearch()
535
+        return true
536
+    }
537
+
497 538
     private func applySearchFieldInsertionPoint(_ object: Any?) {
498 539
         guard let field = object as? NSTextField,
499 540
               field === jobKeywordsField,
@@ -692,6 +733,11 @@ final class DashboardView: NSView, NSTextFieldDelegate {
692 733
 
693 734
 }
694 735
 
736
+/// Document view for the job list `NSScrollView`; flipped coordinates keep short result sets aligned to the top of the clip (avoids a large empty band above the cards on macOS).
737
+private final class JobListingsDocumentView: NSView {
738
+    override var isFlipped: Bool { true }
739
+}
740
+
695 741
 /// Captures clicks for the full sidebar pill so icon, label, and padding behave as one tab.
696 742
 private final class SidebarNavRowView: NSView {
697 743
     private let onSelect: () -> Void