|
|
@@ -6,6 +6,11 @@
|
|
6
|
6
|
import Cocoa
|
|
7
|
7
|
import QuartzCore
|
|
8
|
8
|
|
|
|
9
|
+private enum JobListingCardContext {
|
|
|
10
|
+ case homeSearchResults
|
|
|
11
|
+ case savedJobsPage
|
|
|
12
|
+}
|
|
|
13
|
+
|
|
9
|
14
|
final class DashboardView: NSView, NSTextFieldDelegate {
|
|
10
|
15
|
/// Indeed.com-inspired neutrals and brand blue (white surfaces, `#2557a7` accent, `#2d2d2d` / `#767676` text, `#d4d2d0` borders).
|
|
11
|
16
|
private enum Theme {
|
|
|
@@ -55,8 +60,15 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
55
|
60
|
private let jobListingsStack = NSStackView()
|
|
56
|
61
|
/// Shown when a sidebar item other than Home is selected.
|
|
57
|
62
|
private let nonHomeHost = NSView()
|
|
|
63
|
+ private let nonHomeGenericContainer = NSView()
|
|
58
|
64
|
private let nonHomeTitleLabel = NSTextField(labelWithString: "")
|
|
59
|
65
|
private let nonHomeSubtitleLabel = NSTextField(wrappingLabelWithString: "")
|
|
|
66
|
+ private let savedJobsPageContainer = NSView()
|
|
|
67
|
+ private let savedJobsPageTitleLabel = NSTextField(labelWithString: "Saved Jobs")
|
|
|
68
|
+ private let savedJobsPageSubtitleLabel = NSTextField(wrappingLabelWithString: "")
|
|
|
69
|
+ private let savedJobsScrollView = NSScrollView()
|
|
|
70
|
+ private let savedJobsDocumentView = JobListingsDocumentView()
|
|
|
71
|
+ private let savedJobsStack = NSStackView()
|
|
60
|
72
|
|
|
61
|
73
|
private var currentSidebarItems: [SidebarItem] = []
|
|
62
|
74
|
private var selectedSidebarIndex: Int = 0
|
|
|
@@ -65,7 +77,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
65
|
77
|
/// Last successful search result set (used when removing a card with the dismiss control).
|
|
66
|
78
|
private var lastSearchResults: [JobListing] = []
|
|
67
|
79
|
private var lastNoResultsQuery: String?
|
|
68
|
|
- private var savedJobs: Set<JobListing> = []
|
|
|
80
|
+ /// Most recently saved jobs appear first; persisted across launches.
|
|
|
81
|
+ private var savedJobOrder: [JobListing] = []
|
|
69
|
82
|
|
|
70
|
83
|
override init(frame frameRect: NSRect) {
|
|
71
|
84
|
super.init(frame: frameRect)
|
|
|
@@ -94,7 +107,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
94
|
107
|
}
|
|
95
|
108
|
configureSidebar()
|
|
96
|
109
|
catalogJobListings = data.jobListings
|
|
97
|
|
- savedJobs = []
|
|
|
110
|
+ savedJobOrder = Self.normalizedSavedJobs(SavedJobsStore.load())
|
|
98
|
111
|
configureJobListings([], noResultsForQuery: nil)
|
|
99
|
112
|
updateMainContentVisibility()
|
|
100
|
113
|
}
|
|
|
@@ -251,12 +264,16 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
251
|
264
|
}
|
|
252
|
265
|
|
|
253
|
266
|
private func updateJobListingDescriptionWidths() {
|
|
254
|
|
- let containerWidth = jobListingsContainer.bounds.width
|
|
|
267
|
+ updateDescriptionColumnWidths(in: jobListingsStack, containerWidth: jobListingsContainer.bounds.width)
|
|
|
268
|
+ updateDescriptionColumnWidths(in: savedJobsStack, containerWidth: savedJobsDocumentView.bounds.width)
|
|
|
269
|
+ }
|
|
|
270
|
+
|
|
|
271
|
+ private func updateDescriptionColumnWidths(in stack: NSStackView, containerWidth: CGFloat) {
|
|
255
|
272
|
guard containerWidth > 1 else { return }
|
|
256
|
273
|
let buttonStripReserve: CGFloat = 200
|
|
257
|
274
|
let fallbackTextColumn = max(1, containerWidth - 32 - buttonStripReserve)
|
|
258
|
275
|
var didChange = false
|
|
259
|
|
- for card in jobListingsStack.arrangedSubviews {
|
|
|
276
|
+ for card in stack.arrangedSubviews {
|
|
260
|
277
|
guard let desc = card.viewWithTag(502) as? NSTextField else { continue }
|
|
261
|
278
|
let columnWidth: CGFloat
|
|
262
|
279
|
if let column = desc.superview, column.bounds.width > 1 {
|
|
|
@@ -271,8 +288,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
271
|
288
|
}
|
|
272
|
289
|
}
|
|
273
|
290
|
if didChange {
|
|
274
|
|
- // Wrapping width changed, so card heights need to recompute against the new intrinsic sizes.
|
|
275
|
|
- jobListingsStack.needsLayout = true
|
|
|
291
|
+ stack.needsLayout = true
|
|
276
|
292
|
}
|
|
277
|
293
|
}
|
|
278
|
294
|
|
|
|
@@ -297,14 +313,41 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
297
|
313
|
return
|
|
298
|
314
|
}
|
|
299
|
315
|
for job in jobs {
|
|
300
|
|
- let card = makeJobListingCard(job)
|
|
|
316
|
+ let card = makeJobListingCard(job, context: .homeSearchResults)
|
|
301
|
317
|
jobListingsStack.addArrangedSubview(card)
|
|
302
|
318
|
// Force every card to span the full row instead of hugging its intrinsic content width.
|
|
303
|
319
|
card.widthAnchor.constraint(equalTo: jobListingsStack.widthAnchor).isActive = true
|
|
304
|
320
|
}
|
|
305
|
321
|
}
|
|
306
|
322
|
|
|
307
|
|
- private func makeJobListingCard(_ job: JobListing) -> NSView {
|
|
|
323
|
+ private static func normalizedSavedJobs(_ jobs: [JobListing]) -> [JobListing] {
|
|
|
324
|
+ var seen = Set<JobListing>()
|
|
|
325
|
+ var out: [JobListing] = []
|
|
|
326
|
+ for job in jobs where seen.insert(job).inserted {
|
|
|
327
|
+ out.append(job)
|
|
|
328
|
+ }
|
|
|
329
|
+ return out
|
|
|
330
|
+ }
|
|
|
331
|
+
|
|
|
332
|
+ private func isJobSaved(_ job: JobListing) -> Bool {
|
|
|
333
|
+ savedJobOrder.contains(job)
|
|
|
334
|
+ }
|
|
|
335
|
+
|
|
|
336
|
+ private func persistSavedJobs() {
|
|
|
337
|
+ SavedJobsStore.save(savedJobOrder)
|
|
|
338
|
+ }
|
|
|
339
|
+
|
|
|
340
|
+ private func applySavedState(_ saved: Bool, for job: JobListing) {
|
|
|
341
|
+ if saved {
|
|
|
342
|
+ savedJobOrder.removeAll { $0 == job }
|
|
|
343
|
+ savedJobOrder.insert(job, at: 0)
|
|
|
344
|
+ } else {
|
|
|
345
|
+ savedJobOrder.removeAll { $0 == job }
|
|
|
346
|
+ }
|
|
|
347
|
+ persistSavedJobs()
|
|
|
348
|
+ }
|
|
|
349
|
+
|
|
|
350
|
+ private func makeJobListingCard(_ job: JobListing, context: JobListingCardContext) -> NSView {
|
|
308
|
351
|
let card = NSView()
|
|
309
|
352
|
card.translatesAutoresizingMaskIntoConstraints = false
|
|
310
|
353
|
card.wantsLayer = true
|
|
|
@@ -331,6 +374,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
331
|
374
|
|
|
332
|
375
|
let applyButton = JobPayloadButton(title: "Apply", target: self, action: #selector(didTapJobApply(_:)))
|
|
333
|
376
|
applyButton.jobPayload = job
|
|
|
377
|
+ applyButton.cardContext = context
|
|
334
|
378
|
applyButton.isBordered = false
|
|
335
|
379
|
applyButton.bezelStyle = .rounded
|
|
336
|
380
|
applyButton.font = .systemFont(ofSize: 13, weight: .semibold)
|
|
|
@@ -342,20 +386,23 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
342
|
386
|
applyButton.setContentHuggingPriority(.required, for: .horizontal)
|
|
343
|
387
|
applyButton.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
344
|
388
|
|
|
345
|
|
- let savedButton = JobPayloadButton(title: "Saved", target: self, action: #selector(didTapJobSaved(_:)))
|
|
|
389
|
+ let savedOn = isJobSaved(job)
|
|
|
390
|
+ let savedButton = JobPayloadButton(title: savedOn ? "Saved" : "Save", target: self, action: #selector(didTapJobSaved(_:)))
|
|
346
|
391
|
savedButton.jobPayload = job
|
|
|
392
|
+ savedButton.cardContext = context
|
|
347
|
393
|
savedButton.setButtonType(.toggle)
|
|
348
|
394
|
savedButton.isBordered = false
|
|
349
|
395
|
savedButton.bezelStyle = .rounded
|
|
350
|
396
|
savedButton.font = .systemFont(ofSize: 13, weight: .semibold)
|
|
351
|
397
|
savedButton.focusRingType = .none
|
|
352
|
|
- savedButton.state = savedJobs.contains(job) ? .on : .off
|
|
|
398
|
+ savedButton.state = savedOn ? .on : .off
|
|
353
|
399
|
styleJobSavedButton(savedButton)
|
|
354
|
400
|
savedButton.setContentHuggingPriority(.required, for: .horizontal)
|
|
355
|
401
|
savedButton.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
356
|
402
|
|
|
357
|
403
|
let dismissButton = JobPayloadButton()
|
|
358
|
404
|
dismissButton.jobPayload = job
|
|
|
405
|
+ dismissButton.cardContext = context
|
|
359
|
406
|
dismissButton.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Dismiss")
|
|
360
|
407
|
dismissButton.imagePosition = .imageOnly
|
|
361
|
408
|
dismissButton.imageScaling = .scaleProportionallyDown
|
|
|
@@ -365,7 +412,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
365
|
412
|
dismissButton.contentTintColor = Theme.secondaryText
|
|
366
|
413
|
dismissButton.target = self
|
|
367
|
414
|
dismissButton.action = #selector(didTapJobDismiss(_:))
|
|
368
|
|
- dismissButton.toolTip = "Dismiss"
|
|
|
415
|
+ dismissButton.toolTip = context == .savedJobsPage ? "Remove from saved" : "Dismiss"
|
|
369
|
416
|
dismissButton.focusRingType = .none
|
|
370
|
417
|
dismissButton.setContentHuggingPriority(.required, for: .horizontal)
|
|
371
|
418
|
|
|
|
@@ -446,19 +493,29 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
446
|
493
|
|
|
447
|
494
|
@objc private func didTapJobSaved(_ sender: NSButton) {
|
|
448
|
495
|
guard let job = (sender as? JobPayloadButton)?.jobPayload else { return }
|
|
449
|
|
- if savedJobs.contains(job) {
|
|
450
|
|
- savedJobs.remove(job)
|
|
451
|
|
- } else {
|
|
452
|
|
- savedJobs.insert(job)
|
|
453
|
|
- }
|
|
454
|
|
- sender.state = savedJobs.contains(job) ? .on : .off
|
|
|
496
|
+ let willSave = !isJobSaved(job)
|
|
|
497
|
+ applySavedState(willSave, for: job)
|
|
|
498
|
+ sender.state = willSave ? .on : .off
|
|
|
499
|
+ sender.title = willSave ? "Saved" : "Save"
|
|
455
|
500
|
styleJobSavedButton(sender)
|
|
|
501
|
+ if isSavedJobsSidebarIndex(selectedSidebarIndex) {
|
|
|
502
|
+ reloadSavedJobsListings()
|
|
|
503
|
+ }
|
|
456
|
504
|
}
|
|
457
|
505
|
|
|
458
|
506
|
@objc private func didTapJobDismiss(_ sender: NSButton) {
|
|
459
|
|
- guard let job = (sender as? JobPayloadButton)?.jobPayload else { return }
|
|
460
|
|
- lastSearchResults.removeAll { $0 == job }
|
|
461
|
|
- configureJobListings(lastSearchResults, noResultsForQuery: lastNoResultsQuery)
|
|
|
507
|
+ guard let button = sender as? JobPayloadButton, let job = button.jobPayload else { return }
|
|
|
508
|
+ switch button.cardContext {
|
|
|
509
|
+ case .homeSearchResults:
|
|
|
510
|
+ lastSearchResults.removeAll { $0 == job }
|
|
|
511
|
+ configureJobListings(lastSearchResults, noResultsForQuery: lastNoResultsQuery)
|
|
|
512
|
+ case .savedJobsPage:
|
|
|
513
|
+ applySavedState(false, for: job)
|
|
|
514
|
+ reloadSavedJobsListings()
|
|
|
515
|
+ if isHomeSidebarIndex(selectedSidebarIndex), !lastSearchResults.isEmpty {
|
|
|
516
|
+ configureJobListings(lastSearchResults, noResultsForQuery: lastNoResultsQuery, updateLastResults: false)
|
|
|
517
|
+ }
|
|
|
518
|
+ }
|
|
462
|
519
|
}
|
|
463
|
520
|
|
|
464
|
521
|
private func configureSearchBar() {
|
|
|
@@ -630,6 +687,23 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
630
|
687
|
nonHomeHost.layer?.backgroundColor = Theme.mainHostBackground.cgColor
|
|
631
|
688
|
nonHomeHost.isHidden = true
|
|
632
|
689
|
|
|
|
690
|
+ nonHomeGenericContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
691
|
+ savedJobsPageContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
692
|
+ nonHomeHost.addSubview(nonHomeGenericContainer)
|
|
|
693
|
+ nonHomeHost.addSubview(savedJobsPageContainer)
|
|
|
694
|
+
|
|
|
695
|
+ NSLayoutConstraint.activate([
|
|
|
696
|
+ nonHomeGenericContainer.leadingAnchor.constraint(equalTo: nonHomeHost.leadingAnchor),
|
|
|
697
|
+ nonHomeGenericContainer.trailingAnchor.constraint(equalTo: nonHomeHost.trailingAnchor),
|
|
|
698
|
+ nonHomeGenericContainer.topAnchor.constraint(equalTo: nonHomeHost.topAnchor),
|
|
|
699
|
+ nonHomeGenericContainer.bottomAnchor.constraint(equalTo: nonHomeHost.bottomAnchor),
|
|
|
700
|
+
|
|
|
701
|
+ savedJobsPageContainer.leadingAnchor.constraint(equalTo: nonHomeHost.leadingAnchor),
|
|
|
702
|
+ savedJobsPageContainer.trailingAnchor.constraint(equalTo: nonHomeHost.trailingAnchor),
|
|
|
703
|
+ savedJobsPageContainer.topAnchor.constraint(equalTo: nonHomeHost.topAnchor),
|
|
|
704
|
+ savedJobsPageContainer.bottomAnchor.constraint(equalTo: nonHomeHost.bottomAnchor)
|
|
|
705
|
+ ])
|
|
|
706
|
+
|
|
633
|
707
|
nonHomeTitleLabel.font = .systemFont(ofSize: 22, weight: .bold)
|
|
634
|
708
|
nonHomeTitleLabel.textColor = Theme.primaryText
|
|
635
|
709
|
nonHomeTitleLabel.alignment = .center
|
|
|
@@ -641,19 +715,108 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
641
|
715
|
nonHomeSubtitleLabel.maximumNumberOfLines = 0
|
|
642
|
716
|
nonHomeSubtitleLabel.stringValue = "This area is not available in the preview build. Use Home to search jobs."
|
|
643
|
717
|
|
|
644
|
|
- let stack = NSStackView(views: [nonHomeTitleLabel, nonHomeSubtitleLabel])
|
|
645
|
|
- stack.orientation = .vertical
|
|
646
|
|
- stack.spacing = 10
|
|
647
|
|
- stack.alignment = .centerX
|
|
648
|
|
- stack.translatesAutoresizingMaskIntoConstraints = false
|
|
649
|
|
- nonHomeHost.addSubview(stack)
|
|
|
718
|
+ let genericStack = NSStackView(views: [nonHomeTitleLabel, nonHomeSubtitleLabel])
|
|
|
719
|
+ genericStack.orientation = .vertical
|
|
|
720
|
+ genericStack.spacing = 10
|
|
|
721
|
+ genericStack.alignment = .centerX
|
|
|
722
|
+ genericStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
723
|
+ nonHomeGenericContainer.addSubview(genericStack)
|
|
650
|
724
|
NSLayoutConstraint.activate([
|
|
651
|
|
- stack.centerXAnchor.constraint(equalTo: nonHomeHost.centerXAnchor),
|
|
652
|
|
- stack.centerYAnchor.constraint(equalTo: nonHomeHost.centerYAnchor),
|
|
653
|
|
- stack.leadingAnchor.constraint(greaterThanOrEqualTo: nonHomeHost.leadingAnchor, constant: 32),
|
|
654
|
|
- stack.trailingAnchor.constraint(lessThanOrEqualTo: nonHomeHost.trailingAnchor, constant: -32),
|
|
|
725
|
+ genericStack.centerXAnchor.constraint(equalTo: nonHomeGenericContainer.centerXAnchor),
|
|
|
726
|
+ genericStack.centerYAnchor.constraint(equalTo: nonHomeGenericContainer.centerYAnchor),
|
|
|
727
|
+ genericStack.leadingAnchor.constraint(greaterThanOrEqualTo: nonHomeGenericContainer.leadingAnchor, constant: 32),
|
|
|
728
|
+ genericStack.trailingAnchor.constraint(lessThanOrEqualTo: nonHomeGenericContainer.trailingAnchor, constant: -32),
|
|
655
|
729
|
nonHomeSubtitleLabel.widthAnchor.constraint(lessThanOrEqualToConstant: 420)
|
|
656
|
730
|
])
|
|
|
731
|
+
|
|
|
732
|
+ savedJobsPageTitleLabel.font = .systemFont(ofSize: 22, weight: .bold)
|
|
|
733
|
+ savedJobsPageTitleLabel.textColor = Theme.primaryText
|
|
|
734
|
+ savedJobsPageTitleLabel.alignment = .left
|
|
|
735
|
+ savedJobsPageTitleLabel.maximumNumberOfLines = 1
|
|
|
736
|
+
|
|
|
737
|
+ savedJobsPageSubtitleLabel.font = .systemFont(ofSize: 14, weight: .regular)
|
|
|
738
|
+ savedJobsPageSubtitleLabel.textColor = Theme.secondaryText
|
|
|
739
|
+ savedJobsPageSubtitleLabel.alignment = .left
|
|
|
740
|
+ savedJobsPageSubtitleLabel.maximumNumberOfLines = 0
|
|
|
741
|
+
|
|
|
742
|
+ savedJobsDocumentView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
743
|
+ savedJobsStack.orientation = .vertical
|
|
|
744
|
+ savedJobsStack.spacing = 14
|
|
|
745
|
+ savedJobsStack.alignment = .leading
|
|
|
746
|
+ savedJobsStack.distribution = .fill
|
|
|
747
|
+ savedJobsStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
748
|
+ savedJobsStack.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
|
|
749
|
+ savedJobsStack.setHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
750
|
+ savedJobsDocumentView.addSubview(savedJobsStack)
|
|
|
751
|
+ NSLayoutConstraint.activate([
|
|
|
752
|
+ savedJobsStack.leadingAnchor.constraint(equalTo: savedJobsDocumentView.leadingAnchor),
|
|
|
753
|
+ savedJobsStack.trailingAnchor.constraint(equalTo: savedJobsDocumentView.trailingAnchor),
|
|
|
754
|
+ savedJobsStack.topAnchor.constraint(equalTo: savedJobsDocumentView.topAnchor),
|
|
|
755
|
+ savedJobsStack.bottomAnchor.constraint(equalTo: savedJobsDocumentView.bottomAnchor)
|
|
|
756
|
+ ])
|
|
|
757
|
+
|
|
|
758
|
+ savedJobsScrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
759
|
+ savedJobsScrollView.hasVerticalScroller = true
|
|
|
760
|
+ savedJobsScrollView.hasHorizontalScroller = false
|
|
|
761
|
+ savedJobsScrollView.autohidesScrollers = true
|
|
|
762
|
+ savedJobsScrollView.drawsBackground = false
|
|
|
763
|
+ savedJobsScrollView.borderType = .noBorder
|
|
|
764
|
+ savedJobsScrollView.documentView = savedJobsDocumentView
|
|
|
765
|
+ savedJobsScrollView.setContentHuggingPriority(.defaultLow, for: .vertical)
|
|
|
766
|
+ savedJobsScrollView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
|
|
767
|
+
|
|
|
768
|
+ let savedHeaderStack = NSStackView(views: [savedJobsPageTitleLabel, savedJobsPageSubtitleLabel])
|
|
|
769
|
+ savedHeaderStack.orientation = .vertical
|
|
|
770
|
+ savedHeaderStack.spacing = 6
|
|
|
771
|
+ savedHeaderStack.alignment = .leading
|
|
|
772
|
+ savedHeaderStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
773
|
+
|
|
|
774
|
+ let savedOuterStack = NSStackView(views: [savedHeaderStack, savedJobsScrollView])
|
|
|
775
|
+ savedOuterStack.orientation = .vertical
|
|
|
776
|
+ savedOuterStack.spacing = 16
|
|
|
777
|
+ savedOuterStack.alignment = .width
|
|
|
778
|
+ savedOuterStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
779
|
+ savedJobsPageContainer.addSubview(savedOuterStack)
|
|
|
780
|
+ NSLayoutConstraint.activate([
|
|
|
781
|
+ savedOuterStack.leadingAnchor.constraint(equalTo: savedJobsPageContainer.leadingAnchor, constant: 32),
|
|
|
782
|
+ savedOuterStack.trailingAnchor.constraint(equalTo: savedJobsPageContainer.trailingAnchor, constant: -32),
|
|
|
783
|
+ savedOuterStack.topAnchor.constraint(equalTo: savedJobsPageContainer.topAnchor, constant: 8),
|
|
|
784
|
+ savedOuterStack.bottomAnchor.constraint(equalTo: savedJobsPageContainer.bottomAnchor),
|
|
|
785
|
+
|
|
|
786
|
+ savedJobsDocumentView.topAnchor.constraint(equalTo: savedJobsScrollView.contentView.topAnchor),
|
|
|
787
|
+ savedJobsDocumentView.leadingAnchor.constraint(equalTo: savedJobsScrollView.contentView.leadingAnchor),
|
|
|
788
|
+ savedJobsDocumentView.widthAnchor.constraint(equalTo: savedJobsScrollView.contentView.widthAnchor)
|
|
|
789
|
+ ])
|
|
|
790
|
+ }
|
|
|
791
|
+
|
|
|
792
|
+ private func reloadSavedJobsListings() {
|
|
|
793
|
+ savedJobsStack.arrangedSubviews.forEach {
|
|
|
794
|
+ savedJobsStack.removeArrangedSubview($0)
|
|
|
795
|
+ $0.removeFromSuperview()
|
|
|
796
|
+ }
|
|
|
797
|
+ if savedJobOrder.isEmpty {
|
|
|
798
|
+ savedJobsPageSubtitleLabel.stringValue = "Save jobs from Home to see them here."
|
|
|
799
|
+ let empty = NSTextField(wrappingLabelWithString: "No saved jobs yet. Search on Home, then tap Save on a listing.")
|
|
|
800
|
+ empty.font = .systemFont(ofSize: 14, weight: .regular)
|
|
|
801
|
+ empty.textColor = Theme.secondaryText
|
|
|
802
|
+ empty.alignment = .left
|
|
|
803
|
+ empty.maximumNumberOfLines = 0
|
|
|
804
|
+ empty.translatesAutoresizingMaskIntoConstraints = false
|
|
|
805
|
+ savedJobsStack.addArrangedSubview(empty)
|
|
|
806
|
+ empty.widthAnchor.constraint(equalTo: savedJobsStack.widthAnchor).isActive = true
|
|
|
807
|
+ return
|
|
|
808
|
+ }
|
|
|
809
|
+ savedJobsPageSubtitleLabel.stringValue = "\(savedJobOrder.count) saved \(savedJobOrder.count == 1 ? "position" : "positions")"
|
|
|
810
|
+ for job in savedJobOrder {
|
|
|
811
|
+ let card = makeJobListingCard(job, context: .savedJobsPage)
|
|
|
812
|
+ savedJobsStack.addArrangedSubview(card)
|
|
|
813
|
+ card.widthAnchor.constraint(equalTo: savedJobsStack.widthAnchor).isActive = true
|
|
|
814
|
+ }
|
|
|
815
|
+ }
|
|
|
816
|
+
|
|
|
817
|
+ private func isSavedJobsSidebarIndex(_ index: Int) -> Bool {
|
|
|
818
|
+ guard index >= 0, index < currentSidebarItems.count else { return false }
|
|
|
819
|
+ return currentSidebarItems[index].title == "Saved Jobs"
|
|
657
|
820
|
}
|
|
658
|
821
|
|
|
659
|
822
|
private func isHomeSidebarIndex(_ index: Int) -> Bool {
|
|
|
@@ -663,10 +826,17 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
663
|
826
|
|
|
664
|
827
|
private func updateMainContentVisibility() {
|
|
665
|
828
|
let home = isHomeSidebarIndex(selectedSidebarIndex)
|
|
|
829
|
+ let savedJobs = isSavedJobsSidebarIndex(selectedSidebarIndex)
|
|
666
|
830
|
mainOverlay.isHidden = !home
|
|
667
|
831
|
nonHomeHost.isHidden = home
|
|
|
832
|
+ nonHomeGenericContainer.isHidden = savedJobs
|
|
|
833
|
+ savedJobsPageContainer.isHidden = !savedJobs
|
|
668
|
834
|
if !home, selectedSidebarIndex < currentSidebarItems.count {
|
|
669
|
|
- nonHomeTitleLabel.stringValue = currentSidebarItems[selectedSidebarIndex].title
|
|
|
835
|
+ if savedJobs {
|
|
|
836
|
+ reloadSavedJobsListings()
|
|
|
837
|
+ } else {
|
|
|
838
|
+ nonHomeTitleLabel.stringValue = currentSidebarItems[selectedSidebarIndex].title
|
|
|
839
|
+ }
|
|
670
|
840
|
}
|
|
671
|
841
|
}
|
|
672
|
842
|
|
|
|
@@ -950,6 +1120,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
950
|
1120
|
/// `NSButton` that carries a `JobListing` for card actions (`representedObject` is unavailable on `NSButton` in this target).
|
|
951
|
1121
|
private final class JobPayloadButton: NSButton {
|
|
952
|
1122
|
var jobPayload: JobListing?
|
|
|
1123
|
+ var cardContext: JobListingCardContext = .homeSearchResults
|
|
953
|
1124
|
}
|
|
954
|
1125
|
|
|
955
|
1126
|
/// 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).
|