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

Add job listing cards to dashboard with mock data

Introduce JobListing model and jobListings on DashboardData, populate
the mock provider with sample roles, and render stacked cards below the
search bar with wrapping descriptions and layout width updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 недель назад: 3
Родитель
Сommit
c467a700df
2 измененных файлов с 134 добавлено и 0 удалено
  1. 28 0
      App for Indeed/Models/DashboardModels.swift
  2. 106 0
      App for Indeed/Views/DashboardView.swift

+ 28 - 0
App for Indeed/Models/DashboardModels.swift

@@ -11,9 +11,15 @@ struct SidebarItem {
11
     let badge: String?
11
     let badge: String?
12
 }
12
 }
13
 
13
 
14
+struct JobListing {
15
+    let title: String
16
+    let description: String
17
+}
18
+
14
 struct DashboardData {
19
 struct DashboardData {
15
     let subtitle: String
20
     let subtitle: String
16
     let sidebarItems: [SidebarItem]
21
     let sidebarItems: [SidebarItem]
22
+    let jobListings: [JobListing]
17
 }
23
 }
18
 
24
 
19
 protocol DashboardDataProviding {
25
 protocol DashboardDataProviding {
@@ -30,6 +36,28 @@ final class MockDashboardDataProvider: DashboardDataProviding {
30
                 SidebarItem(title: "CV Maker", systemImage: "doc.text", badge: nil),
36
                 SidebarItem(title: "CV Maker", systemImage: "doc.text", badge: nil),
31
                 SidebarItem(title: "Profile", systemImage: "person", badge: nil),
37
                 SidebarItem(title: "Profile", systemImage: "person", badge: nil),
32
                 SidebarItem(title: "Settings", systemImage: "gearshape", badge: nil)
38
                 SidebarItem(title: "Settings", systemImage: "gearshape", badge: nil)
39
+            ],
40
+            jobListings: [
41
+                JobListing(
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."
44
+                ),
45
+                JobListing(
46
+                    title: "Product Designer",
47
+                    description: "Own end-to-end UX for job seeker flows—from discovery to apply. Figma systems, accessibility, and close collaboration with engineering."
48
+                ),
49
+                JobListing(
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."
52
+                ),
53
+                JobListing(
54
+                    title: "Technical Recruiter",
55
+                    description: "Partner with hiring managers to grow engineering teams. Full-cycle recruiting, inclusive sourcing, and a high-trust candidate experience."
56
+                ),
57
+                JobListing(
58
+                    title: "Customer Success Manager",
59
+                    description: "Help employers get the most from their hiring tools. Onboarding, training, and proactive check-ins with a metrics-driven playbook."
60
+                )
33
             ]
61
             ]
34
         )
62
         )
35
     }
63
     }

+ 106 - 0
App for Indeed/Views/DashboardView.swift

@@ -51,6 +51,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
51
     private let findJobsCTAChrome = NSView()
51
     private let findJobsCTAChrome = NSView()
52
     private var findJobsCTAGradientLayer: CAGradientLayer?
52
     private var findJobsCTAGradientLayer: CAGradientLayer?
53
     private let scrollView = NSScrollView()
53
     private let scrollView = NSScrollView()
54
+    private let jobListingsContainer = NSView()
55
+    private let jobListingsStack = NSStackView()
54
 
56
 
55
     private var currentSidebarItems: [SidebarItem] = []
57
     private var currentSidebarItems: [SidebarItem] = []
56
     private var selectedSidebarIndex: Int = 0
58
     private var selectedSidebarIndex: Int = 0
@@ -71,6 +73,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
71
         updateSearchBarShadowPath()
73
         updateSearchBarShadowPath()
72
         findJobsCTAGradientLayer?.frame = findJobsCTAChrome.bounds
74
         findJobsCTAGradientLayer?.frame = findJobsCTAChrome.bounds
73
         updateFindJobsCTAShadowPath()
75
         updateFindJobsCTAShadowPath()
76
+        updateJobListingDescriptionWidths()
74
     }
77
     }
75
 
78
 
76
     func render(_ data: DashboardData) {
79
     func render(_ data: DashboardData) {
@@ -81,6 +84,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
81
             selectedSidebarIndex = max(0, currentSidebarItems.count - 1)
84
             selectedSidebarIndex = max(0, currentSidebarItems.count - 1)
82
         }
85
         }
83
         configureSidebar()
86
         configureSidebar()
87
+        configureJobListings(data.jobListings)
84
         updateDocumentLayout()
88
         updateDocumentLayout()
85
     }
89
     }
86
 
90
 
@@ -163,6 +167,27 @@ final class DashboardView: NSView, NSTextFieldDelegate {
163
         midSpacer.translatesAutoresizingMaskIntoConstraints = false
167
         midSpacer.translatesAutoresizingMaskIntoConstraints = false
164
         midSpacer.heightAnchor.constraint(equalToConstant: 20).isActive = true
168
         midSpacer.heightAnchor.constraint(equalToConstant: 20).isActive = true
165
 
169
 
170
+        let listingsTopSpacer = NSView()
171
+        listingsTopSpacer.translatesAutoresizingMaskIntoConstraints = false
172
+        listingsTopSpacer.heightAnchor.constraint(equalToConstant: 28).isActive = true
173
+
174
+        jobListingsContainer.translatesAutoresizingMaskIntoConstraints = false
175
+        jobListingsStack.orientation = .vertical
176
+        jobListingsStack.spacing = 14
177
+        // `.leading` keeps cards left-anchored; explicit width constraints below stretch each card across the full row.
178
+        jobListingsStack.alignment = .leading
179
+        jobListingsStack.distribution = .fill
180
+        jobListingsStack.translatesAutoresizingMaskIntoConstraints = false
181
+        jobListingsStack.setContentHuggingPriority(.defaultHigh, for: .vertical)
182
+        jobListingsStack.setHuggingPriority(.defaultLow, for: .horizontal)
183
+        jobListingsContainer.addSubview(jobListingsStack)
184
+        NSLayoutConstraint.activate([
185
+            jobListingsStack.leadingAnchor.constraint(equalTo: jobListingsContainer.leadingAnchor),
186
+            jobListingsStack.trailingAnchor.constraint(equalTo: jobListingsContainer.trailingAnchor),
187
+            jobListingsStack.topAnchor.constraint(equalTo: jobListingsContainer.topAnchor),
188
+            jobListingsStack.bottomAnchor.constraint(equalTo: jobListingsContainer.bottomAnchor)
189
+        ])
190
+
166
         let overlayBottomSpacer = NSView()
191
         let overlayBottomSpacer = NSView()
167
         overlayBottomSpacer.translatesAutoresizingMaskIntoConstraints = false
192
         overlayBottomSpacer.translatesAutoresizingMaskIntoConstraints = false
168
         overlayBottomSpacer.setContentHuggingPriority(.defaultLow, for: .vertical)
193
         overlayBottomSpacer.setContentHuggingPriority(.defaultLow, for: .vertical)
@@ -172,6 +197,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
172
         mainOverlay.addArrangedSubview(titleBlock)
197
         mainOverlay.addArrangedSubview(titleBlock)
173
         mainOverlay.addArrangedSubview(midSpacer)
198
         mainOverlay.addArrangedSubview(midSpacer)
174
         mainOverlay.addArrangedSubview(searchBarShadowHost)
199
         mainOverlay.addArrangedSubview(searchBarShadowHost)
200
+        mainOverlay.addArrangedSubview(listingsTopSpacer)
201
+        mainOverlay.addArrangedSubview(jobListingsContainer)
175
         mainOverlay.addArrangedSubview(overlayBottomSpacer)
202
         mainOverlay.addArrangedSubview(overlayBottomSpacer)
176
 
203
 
177
         contentStack.addArrangedSubview(sidebar)
204
         contentStack.addArrangedSubview(sidebar)
@@ -202,6 +229,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
202
             mainOverlay.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
229
             mainOverlay.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
203
 
230
 
204
             searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
231
             searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
232
+            jobListingsContainer.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
205
 
233
 
206
             greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 24),
234
             greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 24),
207
             greetingLabel.trailingAnchor.constraint(equalTo: mainOverlay.trailingAnchor, constant: -24),
235
             greetingLabel.trailingAnchor.constraint(equalTo: mainOverlay.trailingAnchor, constant: -24),
@@ -210,6 +238,84 @@ final class DashboardView: NSView, NSTextFieldDelegate {
210
         ])
238
         ])
211
     }
239
     }
212
 
240
 
241
+    private func updateJobListingDescriptionWidths() {
242
+        let containerWidth = jobListingsContainer.bounds.width
243
+        guard containerWidth > 1 else { return }
244
+        let innerWidth = containerWidth - 32
245
+        var didChange = false
246
+        for card in jobListingsStack.arrangedSubviews {
247
+            guard let desc = card.viewWithTag(502) as? NSTextField else { continue }
248
+            if abs(desc.preferredMaxLayoutWidth - innerWidth) > 0.5 {
249
+                desc.preferredMaxLayoutWidth = innerWidth
250
+                desc.invalidateIntrinsicContentSize()
251
+                didChange = true
252
+            }
253
+        }
254
+        if didChange {
255
+            // Wrapping width changed, so card heights need to recompute against the new intrinsic sizes.
256
+            jobListingsStack.needsLayout = true
257
+        }
258
+    }
259
+
260
+    private func configureJobListings(_ jobs: [JobListing]) {
261
+        jobListingsStack.arrangedSubviews.forEach {
262
+            jobListingsStack.removeArrangedSubview($0)
263
+            $0.removeFromSuperview()
264
+        }
265
+        for job in jobs {
266
+            let card = makeJobListingCard(job)
267
+            jobListingsStack.addArrangedSubview(card)
268
+            // Force every card to span the full row instead of hugging its intrinsic content width.
269
+            card.widthAnchor.constraint(equalTo: jobListingsStack.widthAnchor).isActive = true
270
+        }
271
+    }
272
+
273
+    private func makeJobListingCard(_ job: JobListing) -> NSView {
274
+        let card = NSView()
275
+        card.translatesAutoresizingMaskIntoConstraints = false
276
+        card.wantsLayer = true
277
+        card.layer?.backgroundColor = Theme.cardBackground.cgColor
278
+        card.layer?.cornerRadius = 12
279
+        card.layer?.borderWidth = 1
280
+        card.layer?.borderColor = Theme.border.cgColor
281
+        card.layer?.masksToBounds = true
282
+
283
+        let titleField = NSTextField(labelWithString: job.title)
284
+        titleField.font = .systemFont(ofSize: 16, weight: .semibold)
285
+        titleField.textColor = Theme.primaryText
286
+        titleField.maximumNumberOfLines = 2
287
+        titleField.lineBreakMode = .byWordWrapping
288
+        titleField.translatesAutoresizingMaskIntoConstraints = false
289
+
290
+        let descriptionField = NSTextField(wrappingLabelWithString: job.description)
291
+        descriptionField.font = .systemFont(ofSize: 13, weight: .regular)
292
+        descriptionField.textColor = Theme.secondaryText
293
+        descriptionField.maximumNumberOfLines = 0
294
+        descriptionField.tag = 502
295
+        descriptionField.translatesAutoresizingMaskIntoConstraints = false
296
+
297
+        let inner = NSStackView(views: [titleField, descriptionField])
298
+        inner.orientation = .vertical
299
+        inner.spacing = 6
300
+        inner.alignment = .leading
301
+        inner.translatesAutoresizingMaskIntoConstraints = false
302
+
303
+        card.addSubview(inner)
304
+        NSLayoutConstraint.activate([
305
+            inner.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
306
+            inner.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
307
+            inner.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
308
+            inner.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
309
+
310
+            titleField.leadingAnchor.constraint(equalTo: inner.leadingAnchor),
311
+            titleField.trailingAnchor.constraint(equalTo: inner.trailingAnchor),
312
+            descriptionField.leadingAnchor.constraint(equalTo: inner.leadingAnchor),
313
+            descriptionField.trailingAnchor.constraint(equalTo: inner.trailingAnchor)
314
+        ])
315
+
316
+        return card
317
+    }
318
+
213
     private func configureSearchBar() {
319
     private func configureSearchBar() {
214
         let pillCorner: CGFloat = 27
320
         let pillCorner: CGFloat = 27
215
         let barHeight: CGFloat = 54
321
         let barHeight: CGFloat = 54