CommentWriterComponents.swift 22 KB

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