import SwiftUI struct TitleOptimizerView: View { @Environment(\.requirePremiumAccess) private var requirePremiumAccess @Bindable var viewModel: TitleOptimizerViewModel var body: some View { VStack(spacing: 0) { header tabBar ScrollView { PostPageBoundary { switch viewModel.selectedTab { case .optimize: optimizeContent case .compare: compareContent case .preview: previewContent } } .padding(.horizontal, 24) .padding(.top, 8) .padding(.bottom, 24) } } .background(AppTheme.background) } // MARK: - Header private var header: some View { HStack(spacing: 14) { ModernSidebarIconView(kind: .titleOptimizer, size: 40) VStack(alignment: .leading, spacing: 2) { Text("Title Optimizer") .font(.system(size: 18, weight: .semibold)) .foregroundStyle(AppTheme.textPrimary) Text("Craft viral Reddit titles with AI") .font(.system(size: 11)) .foregroundStyle(AppTheme.textSecondary) } Spacer() headerActions } .padding(.horizontal, 24) .padding(.vertical, 16) .overlay(alignment: .bottom) { Rectangle() .fill(AppTheme.border) .frame(height: 1) } } private var headerActions: some View { HStack(spacing: 8) { Button { viewModel.resetDraft() } label: { Label("Reset", systemImage: "arrow.counterclockwise") .font(.system(size: 11, weight: .medium)) } .buttonStyle(TitleSecondaryButtonStyle()) Button { viewModel.copyToClipboard() } label: { Label("Copy", systemImage: "doc.on.doc") .font(.system(size: 11, weight: .medium)) } .buttonStyle(TitleSecondaryButtonStyle()) .disabled(viewModel.activeTitle.trimmingCharacters(in: .whitespaces).isEmpty) Button { guard requirePremiumAccess() else { return } Task { await viewModel.optimizeTitles() } } label: { HStack(spacing: 6) { if viewModel.isOptimizing { ProgressView() .controlSize(.small) .tint(.white) } else { Image(systemName: "sparkles") .font(.system(size: 11, weight: .semibold)) } Text(viewModel.isOptimizing ? "Optimizing…" : "Optimize") .font(.system(size: 11, weight: .semibold)) } .foregroundStyle(.white) .padding(.horizontal, 14) .padding(.vertical, 8) .background(viewModel.canOptimize ? AppTheme.accentBlue : AppTheme.accentBlue.opacity(0.4)) .clipShape(RoundedRectangle(cornerRadius: 8)) } .buttonStyle(.plain) .disabled(!viewModel.canOptimize) } } // MARK: - Tab Bar private var tabBar: some View { HStack(spacing: 4) { ForEach(TitleOptimizerTab.allCases) { tab in Button { viewModel.selectedTab = tab } label: { Text(tab.title) .font(.system(size: 11, weight: .semibold)) .foregroundStyle(viewModel.selectedTab == tab ? AppTheme.textPrimary : AppTheme.textSecondary) .padding(.horizontal, 14) .padding(.vertical, 8) .background( viewModel.selectedTab == tab ? AppTheme.cardBackground : Color.clear ) .clipShape(RoundedRectangle(cornerRadius: 8)) } .buttonStyle(.plain) } Spacer() } .padding(.horizontal, 24) .padding(.top, 12) .padding(.bottom, 4) } // MARK: - Optimize private var optimizeContent: some View { HStack(alignment: .top, spacing: 16) { configurationPanel editorPanel } } private var configurationPanel: some View { VStack(spacing: 12) { PostFormCard(title: "Target Subreddit", subtitle: "Tailor titles for the community") { PostFormField( label: "Subreddit", placeholder: "technology", text: $viewModel.draft.subreddit, prefix: "r/" ) } PostFormCard(title: "AI Settings", subtitle: "Guide topic, tone, and style") { VStack(alignment: .leading, spacing: 12) { PostFormField( label: "Topic / Keywords", placeholder: "e.g. macOS productivity tips", text: $viewModel.draft.topic ) VStack(alignment: .leading, spacing: 6) { Text("Tone") .font(.system(size: 10, weight: .medium)) .foregroundStyle(AppTheme.textTertiary) TonePicker(selectedTone: $viewModel.draft.tone) } } } PostFormCard(title: "Title Goal", subtitle: "What kind of hook do you want?") { TitleGoalPicker(selectedGoal: $viewModel.draft.titleGoal) } PostFormCard(title: "Post Context", subtitle: "Optional — affects preview style") { VStack(alignment: .leading, spacing: 12) { PostTypePicker(selectedType: Binding( get: { viewModel.draft.postType }, set: { viewModel.selectPostType($0) } )) PostFormField( label: "Flair", placeholder: "Discussion, Question, Meme…", text: $viewModel.draft.flair ) } } PostFormCard(title: "Post Tags", subtitle: "Reddit content labels") { HStack(spacing: 6) { PostTagToggle( title: "NSFW", systemImage: "exclamationmark.triangle.fill", activeColor: Color(hex: 0xEF4444), isOn: $viewModel.draft.isNSFW ) PostTagToggle( title: "Spoiler", systemImage: "eye.slash.fill", activeColor: AppTheme.textSecondary, isOn: $viewModel.draft.isSpoiler ) PostTagToggle( title: "OC", systemImage: "star.fill", activeColor: AppTheme.accentGreen, isOn: $viewModel.draft.isOC ) } } PostFormCard(title: "Suggestions", subtitle: "How many variants to generate") { TitleVariantCountPicker(selectedCount: $viewModel.draft.variantCount) } if let error = viewModel.errorMessage { PostGeneratorMessageBanner(message: error, isError: true) } else if let success = viewModel.successMessage { PostGeneratorMessageBanner(message: success, isError: false) } } .frame(width: 300) } private var editorPanel: some View { VStack(spacing: 12) { PostFormCard(title: "Original Title", subtitle: "The title you want to improve") { PostFormField( label: "Post Title", placeholder: "Enter your draft title here", text: $viewModel.draft.originalTitle ) } characterCountFooter if let analysis = viewModel.analysis { TitleAnalysisCard(analysis: analysis) } suggestionsSection } .frame(maxWidth: .infinity) } @ViewBuilder private var suggestionsSection: some View { PostFormCard( title: "AI Suggestions", subtitle: viewModel.variants.isEmpty ? "Hit Optimize to generate title variants" : "\(viewModel.variants.count) optimized titles ranked by score" ) { if viewModel.variants.isEmpty { TitleOptimizerEmptyState( icon: "textformat.size", title: "No suggestions yet", subtitle: "Fill in your subreddit, topic, and title,\nthen tap Optimize to get AI-powered variants." ) } else { VStack(spacing: 8) { ForEach(viewModel.variants) { variant in TitleVariantRow( variant: variant, isSelected: viewModel.selectedVariantID == variant.id, onSelect: { viewModel.selectVariant(variant) }, onApply: { viewModel.applyVariant(variant) } ) } } } } } private var characterCountFooter: some View { HStack { Text("\(viewModel.draft.originalTitle.count) / 300") .font(.system(size: 10)) .foregroundStyle( viewModel.draft.originalTitle.count > 300 ? Color(hex: 0xF87171) : AppTheme.textTertiary ) Spacer() Text("Reddit limit: 300 characters for title") .font(.system(size: 10)) .foregroundStyle(AppTheme.textTertiary) } } // MARK: - Compare private var compareContent: some View { VStack(spacing: 16) { if viewModel.variants.isEmpty { TitleOptimizerEmptyState( icon: "arrow.left.arrow.right", title: "Nothing to compare yet", subtitle: "Generate title suggestions on the Optimize tab first." ) } else { compareHeader TitleCompareCard( title: viewModel.draft.originalTitle, score: viewModel.analysis?.overallScore, label: "Original", isOriginal: true, isSelected: viewModel.selectedVariantID == nil ) HStack { Rectangle() .fill(AppTheme.border) .frame(height: 1) Text("vs") .font(.system(size: 10, weight: .semibold)) .foregroundStyle(AppTheme.textTertiary) .padding(.horizontal, 8) Rectangle() .fill(AppTheme.border) .frame(height: 1) } VStack(spacing: 8) { ForEach(viewModel.variants) { variant in TitleCompareCard( title: variant.title, score: variant.score, label: "Variant #\(variantRank(variant))", isOriginal: false, isSelected: viewModel.selectedVariantID == variant.id ) .onTapGesture { viewModel.selectVariant(variant) } } } compareMetadata } } .frame(maxWidth: 640) .frame(maxWidth: .infinity) } private var compareHeader: some View { HStack { VStack(alignment: .leading, spacing: 2) { Text("Side-by-Side Comparison") .font(.system(size: 12, weight: .semibold)) .foregroundStyle(AppTheme.textPrimary) Text("Tap a variant to select it as active") .font(.system(size: 10)) .foregroundStyle(AppTheme.textSecondary) } Spacer() if let best = viewModel.bestVariant { HStack(spacing: 6) { Image(systemName: "trophy.fill") .font(.system(size: 10)) .foregroundStyle(AppTheme.accentYellow) Text("Best: \(best.score)/100") .font(.system(size: 10, weight: .semibold)) .foregroundStyle(AppTheme.textSecondary) } } } } private var compareMetadata: some View { VStack(alignment: .leading, spacing: 8) { metadataRow(label: "Subreddit", value: viewModel.formattedSubreddit) metadataRow(label: "Goal", value: viewModel.draft.titleGoal.title) metadataRow(label: "Tone", value: viewModel.draft.tone.title) metadataRow(label: "Active", value: viewModel.activeTitle) } .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall) .fill(AppTheme.cardBackground) .overlay( RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall) .stroke(AppTheme.cardBorder, lineWidth: 1) ) ) } private func variantRank(_ variant: TitleVariant) -> Int { (viewModel.variants.firstIndex(where: { $0.id == variant.id }) ?? 0) + 1 } // MARK: - Preview private var previewContent: some View { VStack(spacing: 16) { Text("Live Preview") .font(.system(size: 12, weight: .semibold)) .foregroundStyle(AppTheme.textSecondary) .frame(maxWidth: .infinity, alignment: .leading) RedditPostPreviewCard( draft: viewModel.previewDraft, formattedSubreddit: viewModel.formattedSubreddit ) PostFormCard(title: "Feed Preview", subtitle: "How your title looks in a crowded feed") { VStack(spacing: 6) { TitleFeedPreviewRow( title: "Other trending post in this subreddit right now", subreddit: viewModel.formattedSubreddit ) TitleFeedPreviewRow( title: viewModel.activeTitle, subreddit: viewModel.formattedSubreddit, isHighlighted: true ) TitleFeedPreviewRow( title: "Another popular discussion happening nearby", subreddit: viewModel.formattedSubreddit ) } } previewMetadata } .frame(maxWidth: 560) .frame(maxWidth: .infinity) } private var previewMetadata: some View { VStack(alignment: .leading, spacing: 8) { metadataRow(label: "Active Title", value: viewModel.activeTitle) metadataRow(label: "Subreddit", value: viewModel.formattedSubreddit) metadataRow(label: "Goal", value: viewModel.draft.titleGoal.title) metadataRow(label: "Tone", value: viewModel.draft.tone.title) metadataRow(label: "Type", value: viewModel.draft.postType.title) if let score = viewModel.analysis?.overallScore { metadataRow(label: "Score", value: "\(score)/100") } if viewModel.draft.isNSFW || viewModel.draft.isSpoiler || viewModel.draft.isOC { metadataRow( label: "Tags", value: [ viewModel.draft.isNSFW ? "NSFW" : nil, viewModel.draft.isSpoiler ? "Spoiler" : nil, viewModel.draft.isOC ? "OC" : nil, ] .compactMap { $0 } .joined(separator: ", ") ) } } .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall) .fill(AppTheme.cardBackground) .overlay( RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall) .stroke(AppTheme.cardBorder, lineWidth: 1) ) ) } private func metadataRow(label: String, value: String) -> some View { HStack(alignment: .top) { Text(label) .font(.system(size: 10, weight: .medium)) .foregroundStyle(AppTheme.textTertiary) .frame(width: 80, alignment: .leading) Text(value) .font(.system(size: 10)) .foregroundStyle(AppTheme.textSecondary) .fixedSize(horizontal: false, vertical: true) } } } private struct TitleSecondaryButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundStyle(AppTheme.textSecondary) .padding(.horizontal, 12) .padding(.vertical, 8) .background(AppTheme.cardBackground.opacity(configuration.isPressed ? 0.6 : 1)) .clipShape(RoundedRectangle(cornerRadius: 8)) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(AppTheme.cardBorder, lineWidth: 1) ) } } #Preview { TitleOptimizerView(viewModel: TitleOptimizerViewModel()) .frame(width: 880, height: 720) }