| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- //
- // JobRowView.swift
- // indeed_ai_job_search
- //
- import SwiftUI
- struct JobRowView: View {
- let listing: JobListing
- let isSaved: Bool
- var onToggleSave: () -> Void
- var onOpen: (() -> Void)? = nil
- var isCompact: Bool = false
- var showsCardChrome: Bool = true
- private var isUnavailable: Bool {
- listing.availability == .unavailable
- }
- private func openListing() {
- if let onOpen {
- onOpen()
- } else {
- JobListingOpener.open(listing)
- }
- }
- var body: some View {
- HStack(alignment: .top, spacing: AppSpacing.md) {
- CompanyLogoView(listing: listing)
- .opacity(isUnavailable ? 0.45 : 1)
- VStack(alignment: .leading, spacing: AppSpacing.xs) {
- HStack(alignment: .top) {
- VStack(alignment: .leading, spacing: 2) {
- Text(listing.title)
- .font(AppTypography.jobTitle)
- .foregroundStyle(isUnavailable ? AppColors.textTertiary : AppColors.textPrimary)
- .strikethrough(isUnavailable)
- .lineLimit(2)
- Text(listing.company)
- .font(AppTypography.company)
- .foregroundStyle(AppColors.textSecondary)
- }
- Spacer(minLength: AppSpacing.sm)
- // Nested Button keeps save from triggering the card open.
- SaveJobButton(isSaved: isSaved, action: onToggleSave)
- }
- metaRow
- SalaryLabel(salary: listing.salary)
- HStack(spacing: AppSpacing.xs) {
- if listing.isEasyApply { EasyApplyBadge() }
- if listing.isFeatured { FeaturedBadge() }
- if listing.isUrgentlyHiring { UrgentlyHiringBadge() }
- if isUnavailable { UnavailableBadge() }
- Spacer(minLength: 0)
- RelativeDateLabel(date: listing.postedAt)
- }
- .padding(.top, 2)
- Text(openLinkTitle)
- .font(AppTypography.caption)
- .foregroundStyle(AppColors.brand)
- .padding(.top, 4)
- .accessibilityHidden(true)
- }
- }
- .padding(AppSpacing.md)
- .frame(maxWidth: .infinity, alignment: .leading)
- .contentShape(Rectangle())
- .onTapGesture(perform: openListing)
- .background(showsCardChrome ? AppColors.surfaceCard : Color.clear)
- .clipShape(RoundedRectangle(cornerRadius: showsCardChrome ? AppSpacing.cardRadius : 0, style: .continuous))
- .overlay {
- if showsCardChrome {
- RoundedRectangle(cornerRadius: AppSpacing.cardRadius, style: .continuous)
- .stroke(AppColors.border, lineWidth: 1)
- }
- }
- .shadow(
- color: showsCardChrome ? Color.black.opacity(AppShadow.cardOpacity) : .clear,
- radius: showsCardChrome ? AppShadow.cardRadius : 0,
- x: 0,
- y: showsCardChrome ? AppShadow.cardY : 0
- )
- .help("\(openLinkTitle): \(listing.title) at \(listing.company)")
- .accessibilityElement(children: .contain)
- .accessibilityAddTraits(.isButton)
- .accessibilityHint(openAccessibilityHint)
- .accessibilityAction(named: openLinkTitle, openListing)
- }
- /// Direct viewjob when we have a real Indeed `jk`; otherwise company-scoped search.
- private var openLinkTitle: String {
- IndeedBrowseURLBuilder.isLikelyRealJobKey(listing.indeedJobKey)
- ? "Open on Indeed"
- : "Search on Indeed"
- }
- private var openAccessibilityHint: String {
- IndeedBrowseURLBuilder.isLikelyRealJobKey(listing.indeedJobKey)
- ? "Opens this job on Indeed"
- : "Searches Indeed for this title at \(listing.company)"
- }
- private var metaRow: some View {
- HStack(spacing: AppSpacing.sm) {
- if showsLocationSeparately {
- Label(listing.location, systemImage: "mappin")
- Text("•")
- .foregroundStyle(AppColors.textTertiary)
- }
- RemoteWorkBadge(type: listing.remoteType)
- if let employment = listing.employmentType {
- Text("•")
- .foregroundStyle(AppColors.textTertiary)
- Label(employment.displayName, systemImage: "briefcase")
- }
- }
- .font(AppTypography.meta)
- .foregroundStyle(AppColors.textSecondary)
- .lineLimit(1)
- }
- /// Avoid “Remote • Remote” when location text is only the work mode.
- private var showsLocationSeparately: Bool {
- let loc = listing.location.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
- guard !loc.isEmpty else { return false }
- if listing.remoteType == .remote,
- loc == "remote" || loc == "remote only" {
- return false
- }
- return true
- }
- }
- struct JobGridCardView: View {
- let listing: JobListing
- let isSaved: Bool
- var onToggleSave: () -> Void
- var onOpen: (() -> Void)? = nil
- private func openListing() {
- if let onOpen {
- onOpen()
- } else {
- JobListingOpener.open(listing)
- }
- }
- var body: some View {
- VStack(alignment: .leading, spacing: AppSpacing.sm) {
- HStack {
- CompanyLogoView(listing: listing)
- Spacer()
- SaveJobButton(isSaved: isSaved, action: onToggleSave)
- }
- Text(listing.title)
- .font(AppTypography.jobTitle)
- .foregroundStyle(AppColors.textPrimary)
- .lineLimit(2)
- Text(listing.company)
- .font(AppTypography.company)
- .foregroundStyle(AppColors.textSecondary)
- Label(listing.location, systemImage: "mappin")
- .font(AppTypography.meta)
- .foregroundStyle(AppColors.textSecondary)
- .lineLimit(1)
- SalaryLabel(salary: listing.salary)
- Spacer(minLength: 0)
- HStack {
- if listing.isEasyApply { EasyApplyBadge() }
- Spacer()
- RelativeDateLabel(date: listing.postedAt)
- }
- Text(openLinkTitle)
- .font(AppTypography.caption)
- .foregroundStyle(AppColors.brand)
- .accessibilityHidden(true)
- }
- .padding(AppSpacing.md)
- .frame(maxWidth: .infinity, minHeight: 180, alignment: .topLeading)
- .contentShape(Rectangle())
- .onTapGesture(perform: openListing)
- .background(AppColors.surfaceCard)
- .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius, style: .continuous))
- .overlay(
- RoundedRectangle(cornerRadius: AppSpacing.cardRadius, style: .continuous)
- .stroke(AppColors.border, lineWidth: 1)
- )
- .shadow(color: Color.black.opacity(AppShadow.cardOpacity), radius: AppShadow.cardRadius, x: 0, y: AppShadow.cardY)
- .help("\(openLinkTitle): \(listing.title) at \(listing.company)")
- .accessibilityAddTraits(.isButton)
- .accessibilityHint(openAccessibilityHint)
- .accessibilityAction(named: openLinkTitle, openListing)
- }
- private var openLinkTitle: String {
- IndeedBrowseURLBuilder.isLikelyRealJobKey(listing.indeedJobKey)
- ? "Open on Indeed"
- : "Search on Indeed"
- }
- private var openAccessibilityHint: String {
- IndeedBrowseURLBuilder.isLikelyRealJobKey(listing.indeedJobKey)
- ? "Opens this job on Indeed"
- : "Searches Indeed for this title at \(listing.company)"
- }
- }
|