소스 검색

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 2 달 전
부모
커밋
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 {
     let badge: String?
 }
 
+struct JobListing {
+    let title: String
+    let description: String
+}
+
 struct DashboardData {
     let subtitle: String
     let sidebarItems: [SidebarItem]
+    let jobListings: [JobListing]
 }
 
 protocol DashboardDataProviding {
@@ -30,6 +36,28 @@ final class MockDashboardDataProvider: DashboardDataProviding {
                 SidebarItem(title: "CV Maker", systemImage: "doc.text", badge: nil),
                 SidebarItem(title: "Profile", systemImage: "person", badge: nil),
                 SidebarItem(title: "Settings", systemImage: "gearshape", badge: nil)
+            ],
+            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."
+                ),
+                JobListing(
+                    title: "Product Designer",
+                    description: "Own end-to-end UX for job seeker flows—from discovery to apply. Figma systems, accessibility, and close collaboration with engineering."
+                ),
+                JobListing(
+                    title: "Machine Learning Engineer",
+                    description: "Improve search and recommendations using large-scale data. Python, PyTorch, and production ML pipelines; research-to-ship mindset."
+                ),
+                JobListing(
+                    title: "Technical Recruiter",
+                    description: "Partner with hiring managers to grow engineering teams. Full-cycle recruiting, inclusive sourcing, and a high-trust candidate experience."
+                ),
+                JobListing(
+                    title: "Customer Success Manager",
+                    description: "Help employers get the most from their hiring tools. Onboarding, training, and proactive check-ins with a metrics-driven playbook."
+                )
             ]
         )
     }

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

@@ -51,6 +51,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
     private let findJobsCTAChrome = NSView()
     private var findJobsCTAGradientLayer: CAGradientLayer?
     private let scrollView = NSScrollView()
+    private let jobListingsContainer = NSView()
+    private let jobListingsStack = NSStackView()
 
     private var currentSidebarItems: [SidebarItem] = []
     private var selectedSidebarIndex: Int = 0
@@ -71,6 +73,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         updateSearchBarShadowPath()
         findJobsCTAGradientLayer?.frame = findJobsCTAChrome.bounds
         updateFindJobsCTAShadowPath()
+        updateJobListingDescriptionWidths()
     }
 
     func render(_ data: DashboardData) {
@@ -81,6 +84,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
             selectedSidebarIndex = max(0, currentSidebarItems.count - 1)
         }
         configureSidebar()
+        configureJobListings(data.jobListings)
         updateDocumentLayout()
     }
 
@@ -163,6 +167,27 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         midSpacer.translatesAutoresizingMaskIntoConstraints = false
         midSpacer.heightAnchor.constraint(equalToConstant: 20).isActive = true
 
+        let listingsTopSpacer = NSView()
+        listingsTopSpacer.translatesAutoresizingMaskIntoConstraints = false
+        listingsTopSpacer.heightAnchor.constraint(equalToConstant: 28).isActive = true
+
+        jobListingsContainer.translatesAutoresizingMaskIntoConstraints = false
+        jobListingsStack.orientation = .vertical
+        jobListingsStack.spacing = 14
+        // `.leading` keeps cards left-anchored; explicit width constraints below stretch each card across the full row.
+        jobListingsStack.alignment = .leading
+        jobListingsStack.distribution = .fill
+        jobListingsStack.translatesAutoresizingMaskIntoConstraints = false
+        jobListingsStack.setContentHuggingPriority(.defaultHigh, for: .vertical)
+        jobListingsStack.setHuggingPriority(.defaultLow, for: .horizontal)
+        jobListingsContainer.addSubview(jobListingsStack)
+        NSLayoutConstraint.activate([
+            jobListingsStack.leadingAnchor.constraint(equalTo: jobListingsContainer.leadingAnchor),
+            jobListingsStack.trailingAnchor.constraint(equalTo: jobListingsContainer.trailingAnchor),
+            jobListingsStack.topAnchor.constraint(equalTo: jobListingsContainer.topAnchor),
+            jobListingsStack.bottomAnchor.constraint(equalTo: jobListingsContainer.bottomAnchor)
+        ])
+
         let overlayBottomSpacer = NSView()
         overlayBottomSpacer.translatesAutoresizingMaskIntoConstraints = false
         overlayBottomSpacer.setContentHuggingPriority(.defaultLow, for: .vertical)
@@ -172,6 +197,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         mainOverlay.addArrangedSubview(titleBlock)
         mainOverlay.addArrangedSubview(midSpacer)
         mainOverlay.addArrangedSubview(searchBarShadowHost)
+        mainOverlay.addArrangedSubview(listingsTopSpacer)
+        mainOverlay.addArrangedSubview(jobListingsContainer)
         mainOverlay.addArrangedSubview(overlayBottomSpacer)
 
         contentStack.addArrangedSubview(sidebar)
@@ -202,6 +229,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
             mainOverlay.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
 
             searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
+            jobListingsContainer.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
 
             greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 24),
             greetingLabel.trailingAnchor.constraint(equalTo: mainOverlay.trailingAnchor, constant: -24),
@@ -210,6 +238,84 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         ])
     }
 
+    private func updateJobListingDescriptionWidths() {
+        let containerWidth = jobListingsContainer.bounds.width
+        guard containerWidth > 1 else { return }
+        let innerWidth = containerWidth - 32
+        var didChange = false
+        for card in jobListingsStack.arrangedSubviews {
+            guard let desc = card.viewWithTag(502) as? NSTextField else { continue }
+            if abs(desc.preferredMaxLayoutWidth - innerWidth) > 0.5 {
+                desc.preferredMaxLayoutWidth = innerWidth
+                desc.invalidateIntrinsicContentSize()
+                didChange = true
+            }
+        }
+        if didChange {
+            // Wrapping width changed, so card heights need to recompute against the new intrinsic sizes.
+            jobListingsStack.needsLayout = true
+        }
+    }
+
+    private func configureJobListings(_ jobs: [JobListing]) {
+        jobListingsStack.arrangedSubviews.forEach {
+            jobListingsStack.removeArrangedSubview($0)
+            $0.removeFromSuperview()
+        }
+        for job in jobs {
+            let card = makeJobListingCard(job)
+            jobListingsStack.addArrangedSubview(card)
+            // Force every card to span the full row instead of hugging its intrinsic content width.
+            card.widthAnchor.constraint(equalTo: jobListingsStack.widthAnchor).isActive = true
+        }
+    }
+
+    private func makeJobListingCard(_ job: JobListing) -> NSView {
+        let card = NSView()
+        card.translatesAutoresizingMaskIntoConstraints = false
+        card.wantsLayer = true
+        card.layer?.backgroundColor = Theme.cardBackground.cgColor
+        card.layer?.cornerRadius = 12
+        card.layer?.borderWidth = 1
+        card.layer?.borderColor = Theme.border.cgColor
+        card.layer?.masksToBounds = true
+
+        let titleField = NSTextField(labelWithString: job.title)
+        titleField.font = .systemFont(ofSize: 16, weight: .semibold)
+        titleField.textColor = Theme.primaryText
+        titleField.maximumNumberOfLines = 2
+        titleField.lineBreakMode = .byWordWrapping
+        titleField.translatesAutoresizingMaskIntoConstraints = false
+
+        let descriptionField = NSTextField(wrappingLabelWithString: job.description)
+        descriptionField.font = .systemFont(ofSize: 13, weight: .regular)
+        descriptionField.textColor = Theme.secondaryText
+        descriptionField.maximumNumberOfLines = 0
+        descriptionField.tag = 502
+        descriptionField.translatesAutoresizingMaskIntoConstraints = false
+
+        let inner = NSStackView(views: [titleField, descriptionField])
+        inner.orientation = .vertical
+        inner.spacing = 6
+        inner.alignment = .leading
+        inner.translatesAutoresizingMaskIntoConstraints = false
+
+        card.addSubview(inner)
+        NSLayoutConstraint.activate([
+            inner.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
+            inner.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
+            inner.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
+            inner.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
+
+            titleField.leadingAnchor.constraint(equalTo: inner.leadingAnchor),
+            titleField.trailingAnchor.constraint(equalTo: inner.trailingAnchor),
+            descriptionField.leadingAnchor.constraint(equalTo: inner.leadingAnchor),
+            descriptionField.trailingAnchor.constraint(equalTo: inner.trailingAnchor)
+        ])
+
+        return card
+    }
+
     private func configureSearchBar() {
         let pillCorner: CGFloat = 27
         let barHeight: CGFloat = 54