Преглед изворни кода

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 недеља
родитељ
комит
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 11
     let badge: String?
12 12
 }
13 13
 
14
+struct JobListing {
15
+    let title: String
16
+    let description: String
17
+}
18
+
14 19
 struct DashboardData {
15 20
     let subtitle: String
16 21
     let sidebarItems: [SidebarItem]
22
+    let jobListings: [JobListing]
17 23
 }
18 24
 
19 25
 protocol DashboardDataProviding {
@@ -30,6 +36,28 @@ final class MockDashboardDataProvider: DashboardDataProviding {
30 36
                 SidebarItem(title: "CV Maker", systemImage: "doc.text", badge: nil),
31 37
                 SidebarItem(title: "Profile", systemImage: "person", badge: nil),
32 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 51
     private let findJobsCTAChrome = NSView()
52 52
     private var findJobsCTAGradientLayer: CAGradientLayer?
53 53
     private let scrollView = NSScrollView()
54
+    private let jobListingsContainer = NSView()
55
+    private let jobListingsStack = NSStackView()
54 56
 
55 57
     private var currentSidebarItems: [SidebarItem] = []
56 58
     private var selectedSidebarIndex: Int = 0
@@ -71,6 +73,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
71 73
         updateSearchBarShadowPath()
72 74
         findJobsCTAGradientLayer?.frame = findJobsCTAChrome.bounds
73 75
         updateFindJobsCTAShadowPath()
76
+        updateJobListingDescriptionWidths()
74 77
     }
75 78
 
76 79
     func render(_ data: DashboardData) {
@@ -81,6 +84,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
81 84
             selectedSidebarIndex = max(0, currentSidebarItems.count - 1)
82 85
         }
83 86
         configureSidebar()
87
+        configureJobListings(data.jobListings)
84 88
         updateDocumentLayout()
85 89
     }
86 90
 
@@ -163,6 +167,27 @@ final class DashboardView: NSView, NSTextFieldDelegate {
163 167
         midSpacer.translatesAutoresizingMaskIntoConstraints = false
164 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 191
         let overlayBottomSpacer = NSView()
167 192
         overlayBottomSpacer.translatesAutoresizingMaskIntoConstraints = false
168 193
         overlayBottomSpacer.setContentHuggingPriority(.defaultLow, for: .vertical)
@@ -172,6 +197,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
172 197
         mainOverlay.addArrangedSubview(titleBlock)
173 198
         mainOverlay.addArrangedSubview(midSpacer)
174 199
         mainOverlay.addArrangedSubview(searchBarShadowHost)
200
+        mainOverlay.addArrangedSubview(listingsTopSpacer)
201
+        mainOverlay.addArrangedSubview(jobListingsContainer)
175 202
         mainOverlay.addArrangedSubview(overlayBottomSpacer)
176 203
 
177 204
         contentStack.addArrangedSubview(sidebar)
@@ -202,6 +229,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
202 229
             mainOverlay.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
203 230
 
204 231
             searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
232
+            jobListingsContainer.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
205 233
 
206 234
             greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 24),
207 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 319
     private func configureSearchBar() {
214 320
         let pillCorner: CGFloat = 27
215 321
         let barHeight: CGFloat = 54