CommentWriterComponents.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. import SwiftUI
  2. // MARK: - Pickers
  3. struct CommentTypePicker: View {
  4. @Binding var selectedType: CommentType
  5. var body: some View {
  6. HStack(spacing: 8) {
  7. ForEach(CommentType.allCases) { type in
  8. Button {
  9. selectedType = type
  10. } label: {
  11. HStack(spacing: 8) {
  12. Image(systemName: type.systemImage)
  13. .font(.system(size: 12, weight: .semibold))
  14. .foregroundStyle(
  15. selectedType == type ? AppTheme.accentGreen : AppTheme.textSecondary
  16. )
  17. .frame(width: 16)
  18. VStack(alignment: .leading, spacing: 1) {
  19. Text(type.title)
  20. .font(.system(size: 11, weight: .semibold))
  21. .foregroundStyle(AppTheme.textPrimary)
  22. Text(type.subtitle)
  23. .font(.system(size: 9))
  24. .foregroundStyle(AppTheme.textTertiary)
  25. .lineLimit(1)
  26. }
  27. Spacer(minLength: 0)
  28. }
  29. .frame(maxWidth: .infinity, alignment: .leading)
  30. .padding(.horizontal, 10)
  31. .padding(.vertical, 8)
  32. .background(
  33. RoundedRectangle(cornerRadius: 8)
  34. .fill(
  35. selectedType == type
  36. ? AppTheme.accentGreen.opacity(0.15)
  37. : AppTheme.panelBackground
  38. )
  39. )
  40. .overlay(
  41. RoundedRectangle(cornerRadius: 8)
  42. .stroke(
  43. selectedType == type
  44. ? AppTheme.accentGreen.opacity(0.5)
  45. : AppTheme.cardBorder,
  46. lineWidth: 1
  47. )
  48. )
  49. }
  50. .buttonStyle(.plain)
  51. }
  52. }
  53. }
  54. }
  55. struct CommentIntentPicker: View {
  56. @Binding var selectedIntent: CommentIntent
  57. var body: some View {
  58. LazyVGrid(
  59. columns: [GridItem(.flexible()), GridItem(.flexible())],
  60. spacing: 8
  61. ) {
  62. ForEach(CommentIntent.allCases) { intent in
  63. Button {
  64. selectedIntent = intent
  65. } label: {
  66. HStack(spacing: 8) {
  67. Image(systemName: intent.systemImage)
  68. .font(.system(size: 11, weight: .semibold))
  69. .foregroundStyle(
  70. selectedIntent == intent ? AppTheme.accentGreen : AppTheme.textSecondary
  71. )
  72. .frame(width: 14)
  73. VStack(alignment: .leading, spacing: 1) {
  74. Text(intent.title)
  75. .font(.system(size: 11, weight: .semibold))
  76. .foregroundStyle(AppTheme.textPrimary)
  77. Text(intent.subtitle)
  78. .font(.system(size: 9))
  79. .foregroundStyle(AppTheme.textTertiary)
  80. .lineLimit(1)
  81. }
  82. Spacer(minLength: 0)
  83. }
  84. .padding(.horizontal, 10)
  85. .padding(.vertical, 8)
  86. .background(
  87. RoundedRectangle(cornerRadius: 8)
  88. .fill(
  89. selectedIntent == intent
  90. ? AppTheme.accentGreen.opacity(0.15)
  91. : AppTheme.panelBackground
  92. )
  93. )
  94. .overlay(
  95. RoundedRectangle(cornerRadius: 8)
  96. .stroke(
  97. selectedIntent == intent
  98. ? AppTheme.accentGreen.opacity(0.5)
  99. : AppTheme.cardBorder,
  100. lineWidth: 1
  101. )
  102. )
  103. }
  104. .buttonStyle(.plain)
  105. }
  106. }
  107. }
  108. }
  109. struct CommentLengthPicker: View {
  110. @Binding var selectedLength: CommentLength
  111. var body: some View {
  112. HStack(spacing: 8) {
  113. ForEach(CommentLength.allCases) { length in
  114. Button {
  115. selectedLength = length
  116. } label: {
  117. VStack(spacing: 2) {
  118. Text(length.title)
  119. .font(.system(size: 10, weight: .semibold))
  120. Text(length.wordRange)
  121. .font(.system(size: 8))
  122. .foregroundStyle(
  123. selectedLength == length ? .white.opacity(0.8) : AppTheme.textTertiary
  124. )
  125. }
  126. .foregroundStyle(selectedLength == length ? .white : AppTheme.textSecondary)
  127. .padding(.horizontal, 12)
  128. .padding(.vertical, 8)
  129. .frame(maxWidth: .infinity)
  130. .background(
  131. RoundedRectangle(cornerRadius: 8)
  132. .fill(
  133. selectedLength == length
  134. ? AppTheme.accentGreen
  135. : AppTheme.panelBackground
  136. )
  137. )
  138. .overlay(
  139. RoundedRectangle(cornerRadius: 8)
  140. .stroke(AppTheme.cardBorder, lineWidth: 1)
  141. )
  142. }
  143. .buttonStyle(.plain)
  144. }
  145. }
  146. }
  147. }
  148. struct CommentTonePicker: View {
  149. @Binding var selectedTone: PostTone
  150. var body: some View {
  151. ScrollView(.horizontal, showsIndicators: false) {
  152. HStack(spacing: 6) {
  153. ForEach(PostTone.allCases) { tone in
  154. Button {
  155. selectedTone = tone
  156. } label: {
  157. Text(tone.title)
  158. .font(.system(size: 10, weight: .semibold))
  159. .foregroundStyle(selectedTone == tone ? .white : AppTheme.textSecondary)
  160. .padding(.horizontal, 10)
  161. .padding(.vertical, 6)
  162. .background(
  163. Capsule()
  164. .fill(
  165. selectedTone == tone
  166. ? AppTheme.accentGreen
  167. : AppTheme.panelBackground
  168. )
  169. )
  170. .overlay(
  171. Capsule()
  172. .stroke(
  173. selectedTone == tone
  174. ? AppTheme.accentGreen.opacity(0.5)
  175. : AppTheme.cardBorder,
  176. lineWidth: 1
  177. )
  178. )
  179. }
  180. .buttonStyle(.plain)
  181. }
  182. }
  183. }
  184. }
  185. }
  186. struct CommentVariantCountPicker: View {
  187. @Binding var selectedCount: CommentVariantCount
  188. var body: some View {
  189. HStack(spacing: 8) {
  190. ForEach(CommentVariantCount.allCases) { count in
  191. Button {
  192. selectedCount = count
  193. } label: {
  194. Text(count.label)
  195. .font(.system(size: 10, weight: .semibold))
  196. .foregroundStyle(selectedCount == count ? .white : AppTheme.textSecondary)
  197. .frame(maxWidth: .infinity)
  198. .padding(.vertical, 7)
  199. .background(
  200. RoundedRectangle(cornerRadius: 7)
  201. .fill(
  202. selectedCount == count
  203. ? AppTheme.accentGreen
  204. : AppTheme.panelBackground
  205. )
  206. )
  207. .overlay(
  208. RoundedRectangle(cornerRadius: 7)
  209. .stroke(AppTheme.cardBorder, lineWidth: 1)
  210. )
  211. }
  212. .buttonStyle(.plain)
  213. }
  214. }
  215. }
  216. }
  217. struct CommentToggleRow: View {
  218. let title: String
  219. let subtitle: String
  220. @Binding var isOn: Bool
  221. var body: some View {
  222. Toggle(isOn: $isOn) {
  223. VStack(alignment: .leading, spacing: 2) {
  224. Text(title)
  225. .font(.system(size: 11, weight: .medium))
  226. .foregroundStyle(AppTheme.textPrimary)
  227. Text(subtitle)
  228. .font(.system(size: 9))
  229. .foregroundStyle(AppTheme.textTertiary)
  230. }
  231. }
  232. .toggleStyle(.switch)
  233. .tint(AppTheme.accentGreen)
  234. }
  235. }
  236. // MARK: - Reddit Etiquette Card
  237. struct RedditCommentEtiquetteCard: View {
  238. var body: some View {
  239. VStack(alignment: .leading, spacing: 8) {
  240. HStack(spacing: 6) {
  241. Image(systemName: "info.circle.fill")
  242. .font(.system(size: 11))
  243. .foregroundStyle(AppTheme.accentGreen)
  244. Text("Reddit Comment Tips")
  245. .font(.system(size: 11, weight: .semibold))
  246. .foregroundStyle(AppTheme.textPrimary)
  247. }
  248. VStack(alignment: .leading, spacing: 4) {
  249. etiquetteRow("10,000 character limit per comment")
  250. etiquetteRow("Markdown: **bold**, *italic*, > quotes, `code`")
  251. etiquetteRow("Stay on-topic for the subreddit")
  252. etiquetteRow("Be civil — disagree with ideas, not people")
  253. etiquetteRow("Edit with \"Edit:\" to clarify, don't bait-and-switch")
  254. }
  255. }
  256. .padding(12)
  257. .frame(maxWidth: .infinity, alignment: .leading)
  258. .background(
  259. RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
  260. .fill(AppTheme.accentGreen.opacity(0.06))
  261. .overlay(
  262. RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
  263. .stroke(AppTheme.accentGreen.opacity(0.2), lineWidth: 1)
  264. )
  265. )
  266. }
  267. private func etiquetteRow(_ text: String) -> some View {
  268. HStack(alignment: .top, spacing: 6) {
  269. Text("•")
  270. .font(.system(size: 10))
  271. .foregroundStyle(AppTheme.accentGreen)
  272. Text(text)
  273. .font(.system(size: 10))
  274. .foregroundStyle(AppTheme.textSecondary)
  275. .fixedSize(horizontal: false, vertical: true)
  276. }
  277. }
  278. }
  279. // MARK: - Thread Context Card
  280. struct RedditThreadContextCard: View {
  281. let postTitle: String
  282. let postBody: String
  283. let parentComment: String
  284. let commentType: CommentType
  285. let formattedSubreddit: String
  286. var body: some View {
  287. VStack(alignment: .leading, spacing: 10) {
  288. HStack(spacing: 6) {
  289. Image(systemName: "doc.text")
  290. .font(.system(size: 11))
  291. .foregroundStyle(AppTheme.accentOrange)
  292. Text("Thread Context")
  293. .font(.system(size: 11, weight: .semibold))
  294. .foregroundStyle(AppTheme.textPrimary)
  295. }
  296. VStack(alignment: .leading, spacing: 8) {
  297. contextBlock(
  298. label: formattedSubreddit,
  299. content: postTitle.isEmpty ? "Post title" : postTitle,
  300. isTitle: true
  301. )
  302. if !postBody.isEmpty {
  303. contextBlock(label: "Post body", content: postBody, isTitle: false)
  304. }
  305. if commentType == .reply, !parentComment.isEmpty {
  306. contextBlock(label: "Replying to", content: parentComment, isTitle: false, isQuoted: true)
  307. }
  308. }
  309. }
  310. .padding(12)
  311. .frame(maxWidth: .infinity, alignment: .leading)
  312. .background(
  313. RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
  314. .fill(AppTheme.cardBackground)
  315. .overlay(
  316. RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
  317. .stroke(AppTheme.cardBorder, lineWidth: 1)
  318. )
  319. )
  320. }
  321. private func contextBlock(
  322. label: String,
  323. content: String,
  324. isTitle: Bool,
  325. isQuoted: Bool = false
  326. ) -> some View {
  327. VStack(alignment: .leading, spacing: 4) {
  328. Text(label)
  329. .font(.system(size: 9, weight: .semibold))
  330. .foregroundStyle(AppTheme.textTertiary)
  331. if isQuoted {
  332. HStack(spacing: 0) {
  333. Rectangle()
  334. .fill(AppTheme.accentGreen.opacity(0.5))
  335. .frame(width: 3)
  336. Text(content)
  337. .font(.system(size: 10))
  338. .foregroundStyle(AppTheme.textSecondary)
  339. .lineLimit(3)
  340. .padding(.leading, 8)
  341. }
  342. .padding(.vertical, 4)
  343. } else {
  344. Text(content)
  345. .font(.system(size: isTitle ? 11 : 10, weight: isTitle ? .semibold : .regular))
  346. .foregroundStyle(isTitle ? AppTheme.textPrimary : AppTheme.textSecondary)
  347. .lineLimit(isTitle ? 2 : 3)
  348. }
  349. }
  350. }
  351. }
  352. // MARK: - Comment Preview
  353. struct RedditCommentPreviewCard: View {
  354. let commentBody: String
  355. let formattedSubreddit: String
  356. let commentType: CommentType
  357. let parentComment: String
  358. var isReply: Bool = false
  359. var body: some View {
  360. VStack(alignment: .leading, spacing: 0) {
  361. if commentType == .reply, !parentComment.isEmpty {
  362. parentCommentRow
  363. .padding(.leading, 14)
  364. }
  365. commentRow
  366. .padding(.leading, commentType == .reply ? 28 : 0)
  367. }
  368. .background(AppTheme.panelBackground)
  369. .clipShape(RoundedRectangle(cornerRadius: AppTheme.cornerRadius))
  370. .overlay(
  371. RoundedRectangle(cornerRadius: AppTheme.cornerRadius)
  372. .stroke(AppTheme.cardBorder, lineWidth: 1)
  373. )
  374. }
  375. private var parentCommentRow: some View {
  376. HStack(alignment: .top, spacing: 8) {
  377. voteColumn
  378. VStack(alignment: .leading, spacing: 6) {
  379. commentHeader(username: "u/OriginalPoster")
  380. Text(parentComment)
  381. .font(.system(size: 11))
  382. .foregroundStyle(AppTheme.textSecondary)
  383. .lineLimit(3)
  384. .fixedSize(horizontal: false, vertical: true)
  385. commentActions
  386. }
  387. }
  388. .padding(.horizontal, 14)
  389. .padding(.vertical, 12)
  390. .opacity(0.7)
  391. }
  392. private var commentRow: some View {
  393. HStack(alignment: .top, spacing: 8) {
  394. voteColumn
  395. VStack(alignment: .leading, spacing: 6) {
  396. commentHeader(username: "u/ReddoraUser")
  397. if commentBody.isEmpty {
  398. Text("Your comment will appear here…")
  399. .font(.system(size: 12))
  400. .foregroundStyle(AppTheme.textTertiary)
  401. .italic()
  402. } else {
  403. PostMarkdownText(text: commentBody, fontSize: 12, color: AppTheme.textPrimary)
  404. }
  405. commentActions
  406. }
  407. }
  408. .padding(.horizontal, 14)
  409. .padding(.vertical, 12)
  410. .background(
  411. commentType == .reply
  412. ? AppTheme.accentGreen.opacity(0.04)
  413. : Color.clear
  414. )
  415. }
  416. private var voteColumn: some View {
  417. VStack(spacing: 2) {
  418. Image(systemName: "arrow.up")
  419. .font(.system(size: 12, weight: .semibold))
  420. .foregroundStyle(AppTheme.textTertiary)
  421. Text("1")
  422. .font(.system(size: 10, weight: .semibold))
  423. .foregroundStyle(AppTheme.textSecondary)
  424. Image(systemName: "arrow.down")
  425. .font(.system(size: 12, weight: .semibold))
  426. .foregroundStyle(AppTheme.textTertiary)
  427. }
  428. .frame(width: 24)
  429. }
  430. private func commentHeader(username: String) -> some View {
  431. HStack(spacing: 4) {
  432. Text(username)
  433. .font(.system(size: 10, weight: .semibold))
  434. .foregroundStyle(AppTheme.textPrimary)
  435. Text("·")
  436. .foregroundStyle(AppTheme.textTertiary)
  437. Text("just now")
  438. .font(.system(size: 9))
  439. .foregroundStyle(AppTheme.textTertiary)
  440. if commentType == .reply, username == "u/ReddoraUser" {
  441. Text("·")
  442. .foregroundStyle(AppTheme.textTertiary)
  443. Text(formattedSubreddit)
  444. .font(.system(size: 9, weight: .medium))
  445. .foregroundStyle(AppTheme.accentGreen)
  446. }
  447. }
  448. }
  449. private var commentActions: some View {
  450. HStack(spacing: 12) {
  451. actionButton("Reply", icon: "arrowshape.turn.up.left")
  452. actionButton("Share", icon: "square.and.arrow.up")
  453. actionButton("Award", icon: "seal")
  454. actionButton("⋯", icon: nil)
  455. }
  456. .padding(.top, 2)
  457. }
  458. private func actionButton(_ title: String, icon: String?) -> some View {
  459. HStack(spacing: 4) {
  460. if let icon {
  461. Image(systemName: icon)
  462. .font(.system(size: 9, weight: .semibold))
  463. }
  464. Text(title)
  465. .font(.system(size: 9, weight: .semibold))
  466. }
  467. .foregroundStyle(AppTheme.textTertiary)
  468. }
  469. }
  470. // MARK: - Variant Row
  471. struct CommentVariantRow: View {
  472. let variant: CommentVariant
  473. let isSelected: Bool
  474. let onSelect: () -> Void
  475. let onApply: () -> Void
  476. var body: some View {
  477. VStack(alignment: .leading, spacing: 8) {
  478. HStack {
  479. CommentScoreBadge(score: variant.score)
  480. Text(variant.reasoning)
  481. .font(.system(size: 9))
  482. .foregroundStyle(AppTheme.textTertiary)
  483. .lineLimit(1)
  484. Spacer()
  485. HStack(spacing: 6) {
  486. Button("Select", action: onSelect)
  487. .font(.system(size: 9, weight: .semibold))
  488. .foregroundStyle(isSelected ? AppTheme.accentGreen : AppTheme.textSecondary)
  489. .buttonStyle(.plain)
  490. Button("Apply", action: onApply)
  491. .font(.system(size: 9, weight: .semibold))
  492. .foregroundStyle(.white)
  493. .padding(.horizontal, 8)
  494. .padding(.vertical, 4)
  495. .background(AppTheme.accentGreen.opacity(0.8))
  496. .clipShape(RoundedRectangle(cornerRadius: 5))
  497. .buttonStyle(.plain)
  498. }
  499. }
  500. Text(variant.body)
  501. .font(.system(size: 11))
  502. .foregroundStyle(AppTheme.textSecondary)
  503. .lineLimit(4)
  504. .fixedSize(horizontal: false, vertical: true)
  505. }
  506. .padding(12)
  507. .frame(maxWidth: .infinity, alignment: .leading)
  508. .background(
  509. RoundedRectangle(cornerRadius: 8)
  510. .fill(isSelected ? AppTheme.accentGreen.opacity(0.1) : AppTheme.panelBackground)
  511. )
  512. .overlay(
  513. RoundedRectangle(cornerRadius: 8)
  514. .stroke(
  515. isSelected ? AppTheme.accentGreen.opacity(0.4) : AppTheme.cardBorder,
  516. lineWidth: 1
  517. )
  518. )
  519. .onTapGesture(perform: onSelect)
  520. }
  521. }
  522. struct CommentScoreBadge: View {
  523. let score: Int
  524. var body: some View {
  525. Text("\(score)")
  526. .font(.system(size: 10, weight: .bold))
  527. .foregroundStyle(scoreColor)
  528. .padding(.horizontal, 6)
  529. .padding(.vertical, 3)
  530. .background(scoreColor.opacity(0.15))
  531. .clipShape(RoundedRectangle(cornerRadius: 4))
  532. }
  533. private var scoreColor: Color {
  534. switch score {
  535. case 80...: AppTheme.accentGreen
  536. case 60..<80: AppTheme.accentYellow
  537. default: AppTheme.textTertiary
  538. }
  539. }
  540. }
  541. struct CommentWriterEmptyState: View {
  542. let icon: String
  543. let title: String
  544. let subtitle: String
  545. var body: some View {
  546. VStack(spacing: 10) {
  547. Image(systemName: icon)
  548. .font(.system(size: 28))
  549. .foregroundStyle(AppTheme.accentGreen.opacity(0.6))
  550. Text(title)
  551. .font(.system(size: 12, weight: .semibold))
  552. .foregroundStyle(AppTheme.textPrimary)
  553. Text(subtitle)
  554. .font(.system(size: 10))
  555. .foregroundStyle(AppTheme.textSecondary)
  556. .multilineTextAlignment(.center)
  557. }
  558. .frame(maxWidth: .infinity)
  559. .padding(.vertical, 32)
  560. }
  561. }
  562. struct CommentSecondaryButtonStyle: ButtonStyle {
  563. func makeBody(configuration: Configuration) -> some View {
  564. configuration.label
  565. .foregroundStyle(AppTheme.textSecondary)
  566. .padding(.horizontal, 12)
  567. .padding(.vertical, 8)
  568. .background(AppTheme.cardBackground.opacity(configuration.isPressed ? 0.6 : 1))
  569. .clipShape(RoundedRectangle(cornerRadius: 8))
  570. .overlay(
  571. RoundedRectangle(cornerRadius: 8)
  572. .stroke(AppTheme.cardBorder, lineWidth: 1)
  573. )
  574. }
  575. }