Nav apraksta

DashboardView.swift 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. //
  2. // DashboardView.swift
  3. // App for Indeed
  4. //
  5. import Cocoa
  6. final class DashboardView: NSView {
  7. private let contentStack = NSStackView()
  8. private let documentContainer = NSView()
  9. private let chromeContainer = NSView()
  10. private let sidebar = NSStackView()
  11. private let mainColumn = NSStackView()
  12. private let greetingLabel = NSTextField(labelWithString: "")
  13. private let subtitleLabel = NSTextField(labelWithString: "Find your perfect job with the power of AI.")
  14. private let heroCard = NSView()
  15. private let statGrid = NSGridView(views: [[]])
  16. private let recommendationsStack = NSStackView()
  17. private let insightsStack = NSStackView()
  18. private let scrollView = NSScrollView()
  19. private var recommendationsWidthConstraint: NSLayoutConstraint?
  20. private var insightsWidthConstraint: NSLayoutConstraint?
  21. override init(frame frameRect: NSRect) {
  22. super.init(frame: frameRect)
  23. setupLayout()
  24. }
  25. required init?(coder: NSCoder) {
  26. super.init(coder: coder)
  27. setupLayout()
  28. }
  29. override func layout() {
  30. super.layout()
  31. updateDocumentLayout()
  32. }
  33. func render(_ data: DashboardData) {
  34. greetingLabel.stringValue = "Welcome back, \(data.greetingName)! 👋"
  35. configureSidebar(data.sidebarItems)
  36. configureStats(data.stats)
  37. configureRecommendations(data.recommendations)
  38. configureInsights(data.insights)
  39. updateDocumentLayout()
  40. }
  41. private func setupLayout() {
  42. wantsLayer = true
  43. layer?.backgroundColor = NSColor(calibratedRed: 0.03, green: 0.06, blue: 0.14, alpha: 1).cgColor
  44. scrollView.translatesAutoresizingMaskIntoConstraints = false
  45. scrollView.hasVerticalScroller = true
  46. scrollView.drawsBackground = false
  47. addSubview(scrollView)
  48. contentStack.orientation = .horizontal
  49. contentStack.spacing = 20
  50. contentStack.translatesAutoresizingMaskIntoConstraints = false
  51. contentStack.alignment = .top
  52. contentStack.edgeInsets = NSEdgeInsets(top: 24, left: 24, bottom: 24, right: 24)
  53. documentContainer.translatesAutoresizingMaskIntoConstraints = true
  54. documentContainer.autoresizingMask = [.width]
  55. documentContainer.frame = NSRect(x: 0, y: 0, width: 1040, height: 900)
  56. chromeContainer.translatesAutoresizingMaskIntoConstraints = false
  57. chromeContainer.wantsLayer = true
  58. chromeContainer.layer?.backgroundColor = NSColor(calibratedRed: 0.03, green: 0.08, blue: 0.2, alpha: 1).cgColor
  59. chromeContainer.layer?.cornerRadius = 18
  60. documentContainer.addSubview(chromeContainer)
  61. chromeContainer.addSubview(contentStack)
  62. scrollView.documentView = documentContainer
  63. sidebar.orientation = .vertical
  64. sidebar.spacing = 10
  65. sidebar.alignment = .leading
  66. sidebar.wantsLayer = true
  67. sidebar.layer?.backgroundColor = NSColor(calibratedRed: 0.06, green: 0.09, blue: 0.2, alpha: 1).cgColor
  68. sidebar.layer?.cornerRadius = 16
  69. sidebar.edgeInsets = NSEdgeInsets(top: 18, left: 14, bottom: 18, right: 14)
  70. sidebar.translatesAutoresizingMaskIntoConstraints = false
  71. mainColumn.orientation = .vertical
  72. mainColumn.spacing = 14
  73. mainColumn.alignment = .leading
  74. mainColumn.translatesAutoresizingMaskIntoConstraints = false
  75. greetingLabel.font = .systemFont(ofSize: 30, weight: .bold)
  76. greetingLabel.textColor = .white
  77. subtitleLabel.font = .systemFont(ofSize: 14, weight: .regular)
  78. subtitleLabel.textColor = NSColor.white.withAlphaComponent(0.75)
  79. heroCard.wantsLayer = true
  80. heroCard.layer?.backgroundColor = NSColor(calibratedRed: 0.08, green: 0.11, blue: 0.28, alpha: 1).cgColor
  81. heroCard.layer?.cornerRadius = 18
  82. heroCard.translatesAutoresizingMaskIntoConstraints = false
  83. let hero = buildHeroContent()
  84. heroCard.addSubview(hero)
  85. statGrid.translatesAutoresizingMaskIntoConstraints = false
  86. statGrid.rowSpacing = 10
  87. statGrid.columnSpacing = 10
  88. let lowerSection = NSStackView()
  89. lowerSection.orientation = .horizontal
  90. lowerSection.spacing = 12
  91. lowerSection.alignment = .top
  92. lowerSection.distribution = .fill
  93. lowerSection.translatesAutoresizingMaskIntoConstraints = false
  94. let recommendationsBox = sectionBox(title: "Recommended for You", content: recommendationsStack)
  95. let insightsBox = sectionBox(title: "AI Insights", content: insightsStack)
  96. recommendationsBox.translatesAutoresizingMaskIntoConstraints = false
  97. insightsBox.translatesAutoresizingMaskIntoConstraints = false
  98. lowerSection.addArrangedSubview(recommendationsBox)
  99. lowerSection.addArrangedSubview(insightsBox)
  100. mainColumn.addArrangedSubview(greetingLabel)
  101. mainColumn.addArrangedSubview(subtitleLabel)
  102. mainColumn.addArrangedSubview(heroCard)
  103. mainColumn.addArrangedSubview(statGrid)
  104. mainColumn.addArrangedSubview(lowerSection)
  105. contentStack.addArrangedSubview(sidebar)
  106. contentStack.addArrangedSubview(mainColumn)
  107. NSLayoutConstraint.activate([
  108. scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
  109. scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
  110. scrollView.topAnchor.constraint(equalTo: topAnchor),
  111. scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
  112. chromeContainer.topAnchor.constraint(equalTo: documentContainer.topAnchor, constant: 18),
  113. chromeContainer.bottomAnchor.constraint(equalTo: documentContainer.bottomAnchor, constant: -18),
  114. chromeContainer.centerXAnchor.constraint(equalTo: documentContainer.centerXAnchor),
  115. chromeContainer.widthAnchor.constraint(lessThanOrEqualToConstant: 1240),
  116. chromeContainer.widthAnchor.constraint(equalTo: documentContainer.widthAnchor, constant: -36),
  117. contentStack.leadingAnchor.constraint(equalTo: chromeContainer.leadingAnchor),
  118. contentStack.trailingAnchor.constraint(equalTo: chromeContainer.trailingAnchor),
  119. contentStack.topAnchor.constraint(equalTo: chromeContainer.topAnchor),
  120. contentStack.bottomAnchor.constraint(equalTo: chromeContainer.bottomAnchor),
  121. sidebar.widthAnchor.constraint(equalToConstant: 225),
  122. mainColumn.widthAnchor.constraint(greaterThanOrEqualToConstant: 720),
  123. heroCard.widthAnchor.constraint(equalTo: mainColumn.widthAnchor),
  124. heroCard.heightAnchor.constraint(equalToConstant: 140),
  125. hero.leadingAnchor.constraint(equalTo: heroCard.leadingAnchor, constant: 22),
  126. hero.trailingAnchor.constraint(equalTo: heroCard.trailingAnchor, constant: -22),
  127. hero.topAnchor.constraint(equalTo: heroCard.topAnchor, constant: 18),
  128. hero.bottomAnchor.constraint(equalTo: heroCard.bottomAnchor, constant: -18),
  129. lowerSection.widthAnchor.constraint(equalTo: mainColumn.widthAnchor)
  130. ])
  131. recommendationsWidthConstraint = recommendationsBox.widthAnchor.constraint(equalTo: mainColumn.widthAnchor, multiplier: 0.68)
  132. insightsWidthConstraint = insightsBox.widthAnchor.constraint(equalTo: mainColumn.widthAnchor, multiplier: 0.32, constant: -8)
  133. recommendationsWidthConstraint?.isActive = true
  134. insightsWidthConstraint?.isActive = true
  135. }
  136. private func buildHeroContent() -> NSView {
  137. let container = NSStackView()
  138. container.orientation = .horizontal
  139. container.translatesAutoresizingMaskIntoConstraints = false
  140. container.distribution = .fillEqually
  141. let left = NSStackView()
  142. left.orientation = .vertical
  143. left.spacing = 8
  144. left.alignment = .leading
  145. let title = NSTextField(labelWithString: "AI Job Search Assistant")
  146. title.font = .systemFont(ofSize: 24, weight: .semibold)
  147. title.textColor = .white
  148. let body = NSTextField(labelWithString: "Let AI find the best jobs for you on Indeed based on your preferences.")
  149. body.font = .systemFont(ofSize: 12, weight: .regular)
  150. body.textColor = NSColor.white.withAlphaComponent(0.72)
  151. let action = NSButton(title: "Find Jobs with AI", target: nil, action: nil)
  152. action.bezelStyle = .rounded
  153. action.wantsLayer = true
  154. action.layer?.backgroundColor = NSColor(calibratedRed: 0.27, green: 0.42, blue: 1.0, alpha: 1).cgColor
  155. action.layer?.cornerRadius = 8
  156. action.contentTintColor = .white
  157. action.font = .systemFont(ofSize: 13, weight: .semibold)
  158. left.addArrangedSubview(title)
  159. left.addArrangedSubview(body)
  160. left.addArrangedSubview(action)
  161. let right = NSStackView()
  162. right.orientation = .vertical
  163. right.alignment = .trailing
  164. right.addArrangedSubview(tagBubble("Quick"))
  165. right.addArrangedSubview(tagBubble("Smart"))
  166. right.addArrangedSubview(tagBubble("Personalized"))
  167. container.addArrangedSubview(left)
  168. container.addArrangedSubview(right)
  169. return container
  170. }
  171. private func configureSidebar(_ items: [SidebarItem]) {
  172. sidebar.arrangedSubviews.forEach {
  173. sidebar.removeArrangedSubview($0)
  174. $0.removeFromSuperview()
  175. }
  176. let brand = NSTextField(labelWithString: "Indeed AI\nJob Finder")
  177. brand.font = .systemFont(ofSize: 18, weight: .bold)
  178. brand.textColor = .white
  179. sidebar.addArrangedSubview(brand)
  180. let spacer = NSView()
  181. spacer.translatesAutoresizingMaskIntoConstraints = false
  182. spacer.heightAnchor.constraint(equalToConstant: 8).isActive = true
  183. sidebar.addArrangedSubview(spacer)
  184. items.enumerated().forEach { index, item in
  185. let row = NSStackView()
  186. row.orientation = .horizontal
  187. row.spacing = 8
  188. row.alignment = .centerY
  189. row.wantsLayer = true
  190. row.layer?.cornerRadius = 8
  191. row.edgeInsets = NSEdgeInsets(top: 8, left: 10, bottom: 8, right: 10)
  192. if index == 0 {
  193. row.layer?.backgroundColor = NSColor(calibratedRed: 0.22, green: 0.31, blue: 0.85, alpha: 0.4).cgColor
  194. }
  195. let icon = NSImageView()
  196. icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 13, weight: .medium)
  197. icon.image = NSImage(systemSymbolName: item.systemImage, accessibilityDescription: item.title)
  198. icon.contentTintColor = .white
  199. let text = NSTextField(labelWithString: item.title)
  200. text.font = .systemFont(ofSize: 14, weight: .medium)
  201. text.textColor = .white
  202. row.addArrangedSubview(icon)
  203. row.addArrangedSubview(text)
  204. if let badge = item.badge {
  205. let badgeField = NSTextField(labelWithString: badge)
  206. badgeField.font = .systemFont(ofSize: 11, weight: .semibold)
  207. badgeField.textColor = .white
  208. badgeField.wantsLayer = true
  209. badgeField.layer?.backgroundColor = NSColor.systemPurple.cgColor
  210. badgeField.layer?.cornerRadius = 8
  211. badgeField.alignment = .center
  212. badgeField.maximumNumberOfLines = 1
  213. badgeField.lineBreakMode = .byClipping
  214. badgeField.translatesAutoresizingMaskIntoConstraints = false
  215. badgeField.widthAnchor.constraint(equalToConstant: 42).isActive = true
  216. row.addArrangedSubview(NSView())
  217. row.addArrangedSubview(badgeField)
  218. }
  219. sidebar.addArrangedSubview(row)
  220. }
  221. }
  222. private func configureStats(_ stats: [StatCard]) {
  223. statGrid.removeRow(at: 0)
  224. let cards = stats.map { stat -> NSView in
  225. let card = NSStackView()
  226. card.orientation = .vertical
  227. card.spacing = 6
  228. card.edgeInsets = NSEdgeInsets(top: 14, left: 14, bottom: 14, right: 14)
  229. card.wantsLayer = true
  230. card.layer?.backgroundColor = NSColor(calibratedRed: 0.06, green: 0.11, blue: 0.24, alpha: 1).cgColor
  231. card.layer?.cornerRadius = 14
  232. let value = NSTextField(labelWithString: stat.value)
  233. value.font = .systemFont(ofSize: 30, weight: .bold)
  234. value.textColor = .white
  235. let title = NSTextField(labelWithString: stat.title)
  236. title.font = .systemFont(ofSize: 13, weight: .medium)
  237. title.textColor = NSColor.white.withAlphaComponent(0.74)
  238. let trend = NSTextField(labelWithString: stat.trend)
  239. trend.font = .systemFont(ofSize: 12, weight: .semibold)
  240. trend.textColor = NSColor.systemGreen
  241. card.addArrangedSubview(value)
  242. card.addArrangedSubview(title)
  243. card.addArrangedSubview(trend)
  244. card.translatesAutoresizingMaskIntoConstraints = false
  245. card.widthAnchor.constraint(equalToConstant: 185).isActive = true
  246. card.heightAnchor.constraint(equalToConstant: 120).isActive = true
  247. return card
  248. }
  249. statGrid.addRow(with: cards)
  250. }
  251. private func configureRecommendations(_ recommendations: [JobRecommendation]) {
  252. recommendationsStack.orientation = .vertical
  253. recommendationsStack.spacing = 10
  254. recommendationsStack.alignment = .leading
  255. recommendationsStack.arrangedSubviews.forEach {
  256. recommendationsStack.removeArrangedSubview($0)
  257. $0.removeFromSuperview()
  258. }
  259. recommendations.forEach { recommendation in
  260. let row = NSStackView()
  261. row.orientation = .horizontal
  262. row.spacing = 12
  263. row.alignment = .centerY
  264. let icon = NSView()
  265. icon.wantsLayer = true
  266. icon.layer?.cornerRadius = 10
  267. icon.layer?.backgroundColor = NSColor(calibratedRed: 0.19, green: 0.34, blue: 0.9, alpha: 0.9).cgColor
  268. icon.translatesAutoresizingMaskIntoConstraints = false
  269. icon.widthAnchor.constraint(equalToConstant: 38).isActive = true
  270. icon.heightAnchor.constraint(equalToConstant: 38).isActive = true
  271. let textColumn = NSStackView()
  272. textColumn.orientation = .vertical
  273. textColumn.spacing = 2
  274. textColumn.alignment = .leading
  275. let title = NSTextField(labelWithString: recommendation.title)
  276. title.font = .systemFont(ofSize: 15, weight: .semibold)
  277. title.textColor = .white
  278. let subtitle = NSTextField(labelWithString: "\(recommendation.company) • \(recommendation.location)")
  279. subtitle.font = .systemFont(ofSize: 12, weight: .regular)
  280. subtitle.textColor = NSColor.white.withAlphaComponent(0.7)
  281. textColumn.addArrangedSubview(title)
  282. textColumn.addArrangedSubview(subtitle)
  283. let meta = NSStackView()
  284. meta.orientation = .vertical
  285. meta.alignment = .trailing
  286. let match = NSTextField(labelWithString: recommendation.matchRate)
  287. match.font = .systemFont(ofSize: 12, weight: .semibold)
  288. match.textColor = NSColor.systemGreen
  289. let posted = NSTextField(labelWithString: recommendation.postedAgo)
  290. posted.font = .systemFont(ofSize: 11, weight: .regular)
  291. posted.textColor = NSColor.white.withAlphaComponent(0.6)
  292. meta.addArrangedSubview(match)
  293. meta.addArrangedSubview(posted)
  294. row.addArrangedSubview(icon)
  295. row.addArrangedSubview(textColumn)
  296. row.addArrangedSubview(NSView())
  297. row.addArrangedSubview(meta)
  298. recommendationsStack.addArrangedSubview(row)
  299. }
  300. }
  301. private func configureInsights(_ insights: [InsightItem]) {
  302. insightsStack.orientation = .vertical
  303. insightsStack.spacing = 12
  304. insightsStack.alignment = .leading
  305. insightsStack.arrangedSubviews.forEach {
  306. insightsStack.removeArrangedSubview($0)
  307. $0.removeFromSuperview()
  308. }
  309. insights.forEach { insight in
  310. let card = NSStackView()
  311. card.orientation = .vertical
  312. card.spacing = 4
  313. card.edgeInsets = NSEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
  314. card.wantsLayer = true
  315. card.layer?.backgroundColor = NSColor(calibratedRed: 0.07, green: 0.12, blue: 0.22, alpha: 1).cgColor
  316. card.layer?.cornerRadius = 10
  317. let title = NSTextField(labelWithString: insight.title)
  318. title.font = .systemFont(ofSize: 14, weight: .semibold)
  319. title.textColor = .white
  320. let body = NSTextField(labelWithString: insight.description)
  321. body.font = .systemFont(ofSize: 12, weight: .regular)
  322. body.textColor = NSColor.white.withAlphaComponent(0.7)
  323. body.maximumNumberOfLines = 2
  324. body.lineBreakMode = .byWordWrapping
  325. card.addArrangedSubview(title)
  326. card.addArrangedSubview(body)
  327. insightsStack.addArrangedSubview(card)
  328. }
  329. }
  330. private func sectionBox(title: String, content: NSStackView) -> NSView {
  331. let box = NSStackView()
  332. box.orientation = .vertical
  333. box.spacing = 12
  334. box.alignment = .leading
  335. box.edgeInsets = NSEdgeInsets(top: 14, left: 14, bottom: 14, right: 14)
  336. box.wantsLayer = true
  337. box.layer?.backgroundColor = NSColor(calibratedRed: 0.05, green: 0.09, blue: 0.19, alpha: 1).cgColor
  338. box.layer?.cornerRadius = 16
  339. let titleLabel = NSTextField(labelWithString: title)
  340. titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
  341. titleLabel.textColor = .white
  342. box.addArrangedSubview(titleLabel)
  343. box.addArrangedSubview(content)
  344. return box
  345. }
  346. private func tagBubble(_ text: String) -> NSView {
  347. let label = NSTextField(labelWithString: text)
  348. label.font = .systemFont(ofSize: 11, weight: .medium)
  349. label.textColor = NSColor(calibratedRed: 0.75, green: 0.79, blue: 1, alpha: 1)
  350. label.wantsLayer = true
  351. label.layer?.backgroundColor = NSColor(calibratedRed: 0.2, green: 0.24, blue: 0.45, alpha: 0.5).cgColor
  352. label.layer?.cornerRadius = 7
  353. label.alignment = .center
  354. label.translatesAutoresizingMaskIntoConstraints = false
  355. label.widthAnchor.constraint(equalToConstant: 90).isActive = true
  356. return label
  357. }
  358. private func updateDocumentLayout() {
  359. documentContainer.layoutSubtreeIfNeeded()
  360. let fittingHeight = max(chromeContainer.fittingSize.height + 36, bounds.height)
  361. documentContainer.frame = NSRect(x: 0, y: 0, width: bounds.width, height: fittingHeight)
  362. }
  363. }