import SwiftUI #if canImport(UIKit) import UIKit #endif struct FeedbackView: View { @State private var suggestion: String = "" @State private var submittedFeedback: SubmittedFeedback? = nil @State private var isSubmitting: Bool = false @State private var showSubmissionMessage: Bool = false @State private var feedbackCategory: FeedbackCategory = .idea @State private var rating: Int = 0 @State private var wantsResponse: Bool = false @State private var contactEmail: String = "" @FocusState private var focusedField: Field? private let gridColumns = [GridItem(.adaptive(minimum: 120), spacing: 12)] var body: some View { ScrollView { VStack(alignment: .leading, spacing: 24) { headerSection infoSection categorySection ratingSection suggestionSection contactSection Button(action: submitSuggestion) { HStack(spacing: 10) { if isSubmitting { ProgressView() .progressViewStyle(CircularProgressViewStyle()) } Text(isSubmitting ? NSLocalizedString("Отправляем...", comment: "feedback: sending state") : NSLocalizedString("Отправить отзыв", comment: "feedback: submit button")) .fontWeight(.semibold) } .frame(maxWidth: .infinity) .padding() .background(buttonBackgroundColor) .foregroundColor(.white) .cornerRadius(16) } .disabled(!canSubmit) if let submission = submittedFeedback, showSubmissionMessage { successSection(for: submission) .transition(.move(edge: .top).combined(with: .opacity)) } Spacer(minLength: 16) } .padding(.vertical, 32) .padding(.horizontal, 20) } .background(Color(.systemGroupedBackground).ignoresSafeArea()) .navigationTitle(NSLocalizedString("Обратная связь", comment: "feedback: navigation title")) .navigationBarTitleDisplayMode(.inline) .simultaneousGesture( TapGesture().onEnded { dismissKeyboardIfNeeded() } ) .onChange(of: wantsResponse) { wants in if !wants { contactEmail = "" } } } private var headerSection: some View { VStack(alignment: .leading, spacing: 12) { Text(NSLocalizedString("Расскажите о своём опыте", comment: "feedback: header title")) .font(.title2) .fontWeight(.bold) Text(NSLocalizedString("Мы читаем каждый отзыв и используем его, чтобы сделать Yobble полезнее для вас.", comment: "feedback: header subtitle")) .foregroundColor(.secondary) } } private var infoSection: some View { VStack(alignment: .leading, spacing: 8) { Label { Text(NSLocalizedString("Поделитесь идеями, сообщите об ошибке или расскажите, что работает отлично.", comment: "feedback: info detail")) } icon: { Image(systemName: "bubble.left.and.bubble.right.fill") .foregroundColor(.accentColor) } .font(.callout) } .padding() .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 16, style: .continuous) .fill(Color.accentColor.opacity(0.08)) ) } private var categorySection: some View { VStack(alignment: .leading, spacing: 12) { sectionTitle(NSLocalizedString("Что вы хотите обсудить?", comment: "feedback: category title")) LazyVGrid(columns: gridColumns, alignment: .leading, spacing: 12) { ForEach(FeedbackCategory.allCases) { category in categoryButton(for: category) } } } } private var ratingSection: some View { VStack(alignment: .leading, spacing: 12) { sectionTitle(NSLocalizedString("Насколько вам нравится Yobble?", comment: "feedback: rating title")) HStack(spacing: 8) { ForEach(1...5, id: \.self) { value in Button { rating = value } label: { Image(systemName: rating >= value ? "star.fill" : "star") .foregroundColor(rating >= value ? Color.yellow : Color(.systemGray3)) .font(.title3) .padding(.vertical, 4) } .buttonStyle(.plain) .accessibilityLabel(Text(String(format: NSLocalizedString("Оценка %d", comment: "feedback: rating accessibility"), value))) .accessibilityAddTraits(rating == value ? .isSelected : []) } } if rating > 0 { Text(ratingDescription(for: rating)) .font(.caption) .foregroundColor(.secondary) } else { Text(NSLocalizedString("Выберите оценку — это поможет нам понять настроение.", comment: "feedback: rating hint")) .font(.caption) .foregroundColor(.secondary) } } } private var suggestionSection: some View { VStack(alignment: .leading, spacing: 12) { sectionTitle(feedbackCategory.promptTitle) TextEditor(text: $suggestion) .frame(minHeight: 140) .padding(12) .background( RoundedRectangle(cornerRadius: 14, style: .continuous) .fill(Color(.systemBackground)) ) .overlay( RoundedRectangle(cornerRadius: 14, style: .continuous) .stroke(Color(.systemGray4), lineWidth: 1) ) .overlay( Group { if suggestion.isEmpty { Text(feedbackCategory.placeholder) .foregroundColor(.secondary) .padding(.horizontal, 18) .padding(.vertical, 16) .allowsHitTesting(false) } } ) .focused($focusedField, equals: .suggestion) .autocorrectionDisabled(false) Text(String(format: NSLocalizedString("%d символов", comment: "feedback: character count"), suggestion.count)) .font(.caption2) .foregroundColor(.secondary) } } private var contactSection: some View { VStack(alignment: .leading, spacing: 12) { sectionTitle(NSLocalizedString("Нужно ли вам ответить?", comment: "feedback: contact title")) Toggle(NSLocalizedString("Получить ответ от команды", comment: "feedback: contact toggle"), isOn: $wantsResponse) .toggleStyle(SwitchToggleStyle(tint: .accentColor)) if wantsResponse { VStack(alignment: .leading, spacing: 6) { TextField(NSLocalizedString("Ваш e-mail", comment: "feedback: email placeholder"), text: $contactEmail) .textContentType(.emailAddress) .textInputAutocapitalization(.never) .keyboardType(.emailAddress) .autocorrectionDisabled(true) .padding(12) .background( RoundedRectangle(cornerRadius: 14, style: .continuous) .fill(Color(.systemBackground)) ) .overlay( RoundedRectangle(cornerRadius: 14, style: .continuous) .stroke(emailBorderColor, lineWidth: 1) ) .focused($focusedField, equals: .email) if !contactEmail.isEmpty && !emailIsValid { Text(NSLocalizedString("Пожалуйста, введите корректный e-mail.", comment: "feedback: email error")) .font(.caption) .foregroundColor(.red) } else { Text(NSLocalizedString("Мы используем адрес только для ответа на ваш запрос.", comment: "feedback: email hint")) .font(.caption) .foregroundColor(.secondary) } } } } } private var canSubmit: Bool { suggestionIsValid && emailIsValid && !isSubmitting } private var suggestionIsValid: Bool { !suggestion.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } private var emailIsValid: Bool { guard wantsResponse else { return true } let trimmed = contactEmail.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return false } return trimmed.contains("@") && trimmed.contains(".") } private var buttonBackgroundColor: Color { suggestionIsValid && emailIsValid ? Color.accentColor : Color(.systemGray4) } private var emailBorderColor: Color { if contactEmail.isEmpty { return Color(.systemGray4) } return emailIsValid ? Color.accentColor : .red } private func sectionTitle(_ text: String) -> some View { Text(text) .font(.headline) } private func categoryButton(for category: FeedbackCategory) -> some View { let isSelected = feedbackCategory == category return Button { feedbackCategory = category } label: { VStack(alignment: .leading, spacing: 6) { Image(systemName: category.iconName) .font(.subheadline) .foregroundColor(isSelected ? .accentColor : .secondary) Text(category.title) .fontWeight(.semibold) .foregroundColor(.primary) Text(category.subtitle) .font(.caption) .foregroundColor(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.vertical, 14) .padding(.horizontal, 16) .background( RoundedRectangle(cornerRadius: 16, style: .continuous) .fill(isSelected ? Color.accentColor.opacity(0.12) : Color(.systemBackground)) ) .overlay( RoundedRectangle(cornerRadius: 16, style: .continuous) .stroke(isSelected ? Color.accentColor : Color(.systemGray4), lineWidth: isSelected ? 2 : 1) ) } .buttonStyle(.plain) } private func ratingDescription(for rating: Int) -> String { switch rating { case 1: return NSLocalizedString("Что-то пошло не так. Расскажите подробности ниже.", comment: "feedback: rating description 1") case 2: return NSLocalizedString("Мы постараемся всё исправить. Напишите, что смутило.", comment: "feedback: rating description 2") case 3: return NSLocalizedString("Неплохо, но можно лучше — что добавить?", comment: "feedback: rating description 3") case 4: return NSLocalizedString("Спасибо! Что поможет нам добраться до пятёрки?", comment: "feedback: rating description 4") case 5: return NSLocalizedString("Прекрасно! Расскажите, что понравилось больше всего.", comment: "feedback: rating description 5") default: return "" } } private func submitSuggestion() { guard canSubmit else { return } let trimmedSuggestion = suggestion.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedEmail = contactEmail.trimmingCharacters(in: .whitespacesAndNewlines) dismissKeyboardIfNeeded() isSubmitting = true showSubmissionMessage = false DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { submittedFeedback = SubmittedFeedback( suggestion: trimmedSuggestion, category: feedbackCategory, rating: rating, email: wantsResponse ? trimmedEmail : nil ) suggestion = "" rating = 0 if !wantsResponse { contactEmail = "" } withAnimation { showSubmissionMessage = true } isSubmitting = false } } private func successSection(for submission: SubmittedFeedback) -> some View { VStack(alignment: .leading, spacing: 10) { Label { Text(NSLocalizedString("Спасибо! Мы получили ваш отзыв", comment: "feedback: success title")) } icon: { Image(systemName: "checkmark.seal.fill") .foregroundColor(.accentColor) } .font(.headline) Text(String(format: NSLocalizedString("Тема: %@", comment: "feedback: success category"), submission.category.title)) .font(.subheadline) if submission.rating > 0 { Text(String(format: NSLocalizedString("Оценка: %d из 5", comment: "feedback: success rating"), submission.rating)) .font(.subheadline) } if !submission.suggestion.isEmpty { Text(submission.suggestion) .font(.callout) .foregroundColor(.secondary) } if let email = submission.email, !email.isEmpty { Text(String(format: NSLocalizedString("Мы свяжемся с вами по адресу %@, как только ответим.", comment: "feedback: success email"), email)) .font(.footnote) .foregroundColor(.secondary) } } .padding() .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 16, style: .continuous) .fill(Color.accentColor.opacity(0.12)) ) } private func dismissKeyboardIfNeeded() { guard focusedField != nil else { return } focusedField = nil #if canImport(UIKit) UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) #endif } } private extension FeedbackView { enum Field: Hashable { case suggestion case email } struct SubmittedFeedback { let suggestion: String let category: FeedbackCategory let rating: Int let email: String? } enum FeedbackCategory: String, CaseIterable, Identifiable { case idea case bug case praise case content var id: String { rawValue } var title: String { switch self { case .idea: return NSLocalizedString("Идея", comment: "feedback category: idea") case .bug: return NSLocalizedString("Проблема", comment: "feedback category: bug") case .praise: return NSLocalizedString("Похвала", comment: "feedback category: praise") case .content: return NSLocalizedString("Контент", comment: "feedback category: content") } } var subtitle: String { switch self { case .idea: return NSLocalizedString("Предложите, что добавить", comment: "feedback category subtitle: idea") case .bug: return NSLocalizedString("Расскажите, что не работает", comment: "feedback category subtitle: bug") case .praise: return NSLocalizedString("Поделитесь, что понравилось", comment: "feedback category subtitle: praise") case .content: return NSLocalizedString("Сообщите о материалах", comment: "feedback category subtitle: content") } } var iconName: String { switch self { case .idea: return "lightbulb" case .bug: return "ant" case .praise: return "heart.fill" case .content: return "doc.richtext" } } var promptTitle: String { switch self { case .idea: return NSLocalizedString("Опишите идею", comment: "feedback prompt: idea") case .bug: return NSLocalizedString("Что случилось?", comment: "feedback prompt: bug") case .praise: return NSLocalizedString("Что вам понравилось?", comment: "feedback prompt: praise") case .content: return NSLocalizedString("О каком контенте идёт речь?", comment: "feedback prompt: content") } } var placeholder: String { switch self { case .idea: return NSLocalizedString("Например: хотелось бы видеть подборку по интересам...", comment: "feedback placeholder: idea") case .bug: return NSLocalizedString("Например: приложение вылетает, когда я открываю профиль...", comment: "feedback placeholder: bug") case .praise: return NSLocalizedString("Например: понравилась новая лента, потому что...", comment: "feedback placeholder: praise") case .content: return NSLocalizedString("Например: заметил неточную информацию в статье...", comment: "feedback placeholder: content") } } } } struct FeedbackView_Previews: PreviewProvider { static var previews: some View { NavigationView { FeedbackView() .environmentObject(ThemeManager()) } } }