|
|
@@ -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
|