Parcourir la source

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 il y a 2 mois
Parent
commit
9786ce1919

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

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

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

@@ -50,11 +50,14 @@ final class DashboardView: NSView, NSTextFieldDelegate {
     private let findJobsCTAChrome = NSView()
     private var findJobsCTAGradientLayer: CAGradientLayer?
     private let jobListingsScrollView = NSScrollView()
-    private let jobListingsContainer = NSView()
+    /// Flipped so short result lists stay visually under the search bar instead of leaving a gap above the cards.
+    private let jobListingsContainer = JobListingsDocumentView()
     private let jobListingsStack = NSStackView()
 
     private var currentSidebarItems: [SidebarItem] = []
     private var selectedSidebarIndex: Int = 0
+    /// Full list from `DashboardData`; results are shown after the user runs a search.
+    private var catalogJobListings: [JobListing] = []
 
     override init(frame frameRect: NSRect) {
         super.init(frame: frameRect)
@@ -82,7 +85,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
             selectedSidebarIndex = max(0, currentSidebarItems.count - 1)
         }
         configureSidebar()
-        configureJobListings(data.jobListings)
+        catalogJobListings = data.jobListings
+        configureJobListings([], noResultsForQuery: nil)
     }
 
     private func setupLayout() {
@@ -157,7 +161,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
 
         let listingsTopSpacer = NSView()
         listingsTopSpacer.translatesAutoresizingMaskIntoConstraints = false
-        listingsTopSpacer.heightAnchor.constraint(equalToConstant: 28).isActive = true
+        listingsTopSpacer.heightAnchor.constraint(equalToConstant: 12).isActive = true
 
         jobListingsContainer.translatesAutoresizingMaskIntoConstraints = false
         jobListingsStack.orientation = .vertical
@@ -248,11 +252,22 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         }
     }
 
-    private func configureJobListings(_ jobs: [JobListing]) {
+    private func configureJobListings(_ jobs: [JobListing], noResultsForQuery: String?) {
         jobListingsStack.arrangedSubviews.forEach {
             jobListingsStack.removeArrangedSubview($0)
             $0.removeFromSuperview()
         }
+        if jobs.isEmpty, let query = noResultsForQuery, !query.isEmpty {
+            let empty = NSTextField(wrappingLabelWithString: "No jobs match “\(query)”. Try different keywords or browse the full list with an empty search.")
+            empty.font = .systemFont(ofSize: 14, weight: .regular)
+            empty.textColor = Theme.secondaryText
+            empty.alignment = .center
+            empty.maximumNumberOfLines = 0
+            empty.translatesAutoresizingMaskIntoConstraints = false
+            jobListingsStack.addArrangedSubview(empty)
+            empty.widthAnchor.constraint(equalTo: jobListingsStack.widthAnchor).isActive = true
+            return
+        }
         for job in jobs {
             let card = makeJobListingCard(job)
             jobListingsStack.addArrangedSubview(card)
@@ -483,7 +498,25 @@ final class DashboardView: NSView, NSTextFieldDelegate {
     }
 
     @objc private func didSubmitSearch() {
-        // Hook up search submission here when wiring up real data.
+        let query = jobKeywordsField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
+        let results = jobsMatchingSearch(query: query, in: catalogJobListings)
+        let noResultsMessage: String? = (!results.isEmpty || query.isEmpty) ? nil : query
+        configureJobListings(results, noResultsForQuery: noResultsMessage)
+        window?.makeFirstResponder(nil)
+    }
+
+    /// Each whitespace-separated token must appear in the title or description (case-insensitive). Empty query returns the full catalog.
+    private func jobsMatchingSearch(query: String, in jobs: [JobListing]) -> [JobListing] {
+        let tokens = query
+            .lowercased()
+            .split(whereSeparator: { $0.isWhitespace })
+            .map(String.init)
+            .filter { !$0.isEmpty }
+        guard !tokens.isEmpty else { return jobs }
+        return jobs.filter { job in
+            let haystack = "\(job.title) \(job.description)".lowercased()
+            return tokens.allSatisfy { haystack.contains($0) }
+        }
     }
 
     func controlTextDidBeginEditing(_ obj: Notification) {
@@ -494,6 +527,14 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         applySearchFieldInsertionPoint(obj.object)
     }
 
+    func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
+        guard control === jobKeywordsField, commandSelector == #selector(NSResponder.insertNewline(_:)) else {
+            return false
+        }
+        didSubmitSearch()
+        return true
+    }
+
     private func applySearchFieldInsertionPoint(_ object: Any?) {
         guard let field = object as? NSTextField,
               field === jobKeywordsField,
@@ -692,6 +733,11 @@ final class DashboardView: NSView, NSTextFieldDelegate {
 
 }
 
+/// 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).
+private final class JobListingsDocumentView: NSView {
+    override var isFlipped: Bool { true }
+}
+
 /// Captures clicks for the full sidebar pill so icon, label, and padding behave as one tab.
 private final class SidebarNavRowView: NSView {
     private let onSelect: () -> Void