JobBadges.swift 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. //
  2. // JobBadges.swift
  3. // indeed_ai_job_search
  4. //
  5. import AppKit
  6. import SwiftUI
  7. struct RemoteWorkBadge: View {
  8. let type: RemoteType
  9. var body: some View {
  10. Label(type.displayName, systemImage: icon)
  11. .font(AppTypography.meta)
  12. .foregroundStyle(AppColors.textSecondary)
  13. .labelStyle(.titleAndIcon)
  14. }
  15. private var icon: String {
  16. switch type {
  17. case .remote: return "house"
  18. case .hybrid: return "building.2"
  19. case .onsite: return "mappin.and.ellipse"
  20. case .unknown: return "questionmark.circle"
  21. }
  22. }
  23. }
  24. struct SalaryLabel: View {
  25. let salary: SalaryRange?
  26. var body: some View {
  27. if let text = salary?.formatted {
  28. Text(text)
  29. .font(AppTypography.salary)
  30. .foregroundStyle(AppColors.salary)
  31. }
  32. }
  33. }
  34. struct RelativeDateLabel: View {
  35. let date: Date
  36. var body: some View {
  37. Text(Self.relativeString(from: date))
  38. .font(AppTypography.meta)
  39. .foregroundStyle(AppColors.textTertiary)
  40. }
  41. static func relativeString(from date: Date) -> String {
  42. let seconds = max(0, Int(Date().timeIntervalSince(date)))
  43. let days = seconds / 86_400
  44. let hours = seconds / 3_600
  45. if days <= 0 {
  46. if hours <= 0 { return "Just now" }
  47. if hours == 1 { return "1 hour ago" }
  48. return "\(hours) hours ago"
  49. }
  50. if days == 1 { return "1 day ago" }
  51. if days < 7 { return "\(days) days ago" }
  52. let weeks = days / 7
  53. if weeks == 1 { return "1 week ago" }
  54. return "\(weeks) weeks ago"
  55. }
  56. }
  57. struct EasyApplyBadge: View {
  58. var body: some View {
  59. Label("Easy Apply", systemImage: "bolt.fill")
  60. .font(AppTypography.caption)
  61. .foregroundStyle(AppColors.easyApply)
  62. .padding(.horizontal, 8)
  63. .padding(.vertical, 4)
  64. .background(AppColors.brandSoft)
  65. .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
  66. }
  67. }
  68. struct FeaturedBadge: View {
  69. var body: some View {
  70. Text("Featured")
  71. .font(AppTypography.caption)
  72. .foregroundStyle(AppColors.featuredText)
  73. .padding(.horizontal, 8)
  74. .padding(.vertical, 4)
  75. .background(AppColors.featured)
  76. .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
  77. }
  78. }
  79. struct UrgentlyHiringBadge: View {
  80. var body: some View {
  81. Text("Urgently hiring")
  82. .font(AppTypography.caption)
  83. .foregroundStyle(AppColors.urgentText)
  84. .padding(.horizontal, 8)
  85. .padding(.vertical, 4)
  86. .background(AppColors.urgent)
  87. .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
  88. }
  89. }
  90. struct UnavailableBadge: View {
  91. var body: some View {
  92. Text("Unavailable")
  93. .font(AppTypography.caption)
  94. .foregroundStyle(AppColors.unavailable)
  95. .padding(.horizontal, 8)
  96. .padding(.vertical, 4)
  97. .background(AppColors.border)
  98. .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
  99. }
  100. }
  101. struct CompanyLogoView: View {
  102. let listing: JobListing
  103. var body: some View {
  104. ZStack {
  105. RoundedRectangle(cornerRadius: 10, style: .continuous)
  106. .fill(logoBackground)
  107. Image(systemName: listing.logoSymbol)
  108. .font(.system(size: 18, weight: .semibold))
  109. .foregroundStyle(logoForeground)
  110. }
  111. .frame(width: 44, height: 44)
  112. .accessibilityHidden(true)
  113. }
  114. private var logoBackground: Color {
  115. switch listing.company.lowercased() {
  116. case "apple": return Color.black.opacity(0.08)
  117. case "microsoft": return Color.blue.opacity(0.12)
  118. default: return AppColors.brandSoft
  119. }
  120. }
  121. private var logoForeground: Color {
  122. switch listing.company.lowercased() {
  123. case "apple": return AppColors.textPrimary
  124. case "microsoft": return Color.blue
  125. default: return AppColors.brand
  126. }
  127. }
  128. }
  129. struct SaveJobButton: View {
  130. let isSaved: Bool
  131. let action: () -> Void
  132. var body: some View {
  133. Button(action: action) {
  134. Image(systemName: isSaved ? "bookmark.fill" : "bookmark")
  135. .font(.system(size: 14, weight: .medium))
  136. .foregroundStyle(isSaved ? AppColors.brand : AppColors.textTertiary)
  137. .frame(width: 28, height: 28)
  138. .contentShape(Rectangle())
  139. }
  140. .buttonStyle(.plain)
  141. .help(isSaved ? "Remove from saved jobs" : "Save job")
  142. .accessibilityLabel(isSaved ? "Unsave job" : "Save job")
  143. }
  144. }
  145. struct OpenIndeedLinkButton: View {
  146. let listing: JobListing
  147. private var title: String {
  148. IndeedBrowseURLBuilder.isLikelyRealJobKey(listing.indeedJobKey)
  149. ? "Open on Indeed"
  150. : "Search on Indeed"
  151. }
  152. var body: some View {
  153. Button {
  154. JobListingOpener.open(listing)
  155. } label: {
  156. Label(title, systemImage: "arrow.up.right.square")
  157. .font(AppTypography.caption)
  158. .foregroundStyle(AppColors.brand)
  159. }
  160. .buttonStyle(.plain)
  161. .help("\(title): \(listing.title) at \(listing.company)")
  162. .accessibilityLabel(title)
  163. }
  164. }