737 lines
29 KiB
Swift
737 lines
29 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - Daily Question
|
|
|
|
struct DailyQuestionView: View {
|
|
@EnvironmentObject var appState: AppState
|
|
@State private var question: Question?
|
|
@State private var isLoading = true
|
|
@State private var hasAnswered = false
|
|
@State private var showReveal = false
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(spacing: CloserSpacing.lg) {
|
|
if isLoading {
|
|
LoadingView(message: "Loading today's question...")
|
|
.padding(.top, CloserSpacing.xxxl)
|
|
} else if let question = question {
|
|
TodayQuestionHeroView()
|
|
.closerPadding()
|
|
|
|
VStack(spacing: CloserSpacing.lg) {
|
|
Text("Today's question")
|
|
.font(CloserFont.subheadline)
|
|
.foregroundColor(.closerTextSecondary)
|
|
|
|
Text(question.text)
|
|
.font(CloserFont.title2)
|
|
.foregroundColor(.closerText)
|
|
.multilineTextAlignment(.center)
|
|
.closerPadding()
|
|
|
|
if hasAnswered {
|
|
// Awaiting partner
|
|
VStack(spacing: CloserSpacing.md) {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.font(.system(size: 48))
|
|
.foregroundColor(.closerSuccess)
|
|
Text("You've answered!")
|
|
.font(CloserFont.title3)
|
|
.foregroundColor(.closerSuccess)
|
|
Text("Waiting for your partner to answer...")
|
|
.font(CloserFont.callout)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
|
|
Button("Send a gentle reminder") {
|
|
Task {
|
|
try? await FirestoreService.shared.sendGentleReminderCallable()
|
|
}
|
|
}
|
|
.font(CloserFont.footnote)
|
|
.foregroundColor(.closerPrimary)
|
|
}
|
|
} else {
|
|
// Answer options
|
|
if let coupleId = appState.currentCouple?.id {
|
|
QuestionAnswerView(question: question, coupleId: coupleId, onAnswered: {
|
|
withAnimation { hasAnswered = true }
|
|
})
|
|
}
|
|
}
|
|
}
|
|
.padding(CloserSpacing.xl)
|
|
.closerCard()
|
|
.closerPadding()
|
|
|
|
// Partner status
|
|
if hasAnswered {
|
|
PartnerStatusRow(
|
|
displayName: appState.currentUser?.displayName ?? "Partner",
|
|
answered: false,
|
|
lastActive: nil
|
|
)
|
|
.closerPadding()
|
|
}
|
|
|
|
// Reveal button if partner has answered
|
|
if hasAnswered {
|
|
Button("Reveal Partner's Answer") {
|
|
showReveal = true
|
|
}
|
|
.buttonStyle(PrimaryButtonStyle())
|
|
.closerPadding()
|
|
}
|
|
} else {
|
|
EmptyStateView(
|
|
icon: "questionmark.bubble",
|
|
title: "No Question Today",
|
|
message: "Check back later for today's question.",
|
|
action: (title: "Refresh", handler: { Task { await loadQuestion() } })
|
|
)
|
|
}
|
|
}
|
|
}
|
|
.background(Color.closerBackground)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .principal) {
|
|
Text("Daily Question")
|
|
.font(CloserFont.headline)
|
|
.foregroundColor(.closerText)
|
|
}
|
|
}
|
|
.navigationDestination(isPresented: $showReveal) {
|
|
if let coupleId = appState.currentCouple?.id {
|
|
AnswerRevealView(questionId: question?.id ?? "", coupleId: coupleId)
|
|
}
|
|
}
|
|
.task {
|
|
await loadQuestion()
|
|
}
|
|
}
|
|
|
|
private func loadQuestion() async {
|
|
isLoading = true
|
|
defer { isLoading = false }
|
|
|
|
// Fetch daily question from Firestore
|
|
guard let coupleId = appState.currentCouple?.id else { return }
|
|
let today = dateString()
|
|
|
|
do {
|
|
let dq: DailyQuestion? = try await FirestoreService.shared.getDocument(
|
|
at: FirestoreService.shared.dailyQuestionRef(coupleId: coupleId, date: today)
|
|
)
|
|
|
|
if let questionId = dq?.questionId {
|
|
// Fetch question from local bundle or Firestore
|
|
self.question = bundledQuestions.first { $0.id == questionId }
|
|
// Check if user already answered
|
|
if let userId = AuthService.shared.currentUserId {
|
|
let answer: DailyAnswer? = try await FirestoreService.shared.getDocument(
|
|
at: FirestoreService.shared.dailyAnswerRef(coupleId: coupleId, date: today, userId: userId)
|
|
)
|
|
hasAnswered = answer != nil && answer?.submittedAt.timeIntervalSince1970 ?? 0 > 0
|
|
}
|
|
}
|
|
} catch {
|
|
// Fall back to bundled question
|
|
self.question = bundledQuestions.randomElement()
|
|
}
|
|
}
|
|
|
|
private func dateString() -> String {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "yyyy-MM-dd"
|
|
return formatter.string(from: Date())
|
|
}
|
|
}
|
|
|
|
private struct TodayQuestionHeroView: View {
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: CloserSpacing.md) {
|
|
Image("illustration-daily-question")
|
|
.resizable()
|
|
.scaledToFill()
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: 174)
|
|
.clipShape(RoundedRectangle(cornerRadius: CloserRadius.xlarge, style: .continuous))
|
|
.accessibilityHidden(true)
|
|
|
|
VStack(alignment: .leading, spacing: CloserSpacing.xs) {
|
|
Text("One question, enough space")
|
|
.font(CloserFont.title2)
|
|
.foregroundColor(.closerText)
|
|
|
|
Text("Answer privately first, then reveal when you are both ready.")
|
|
.font(CloserFont.callout)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
.padding(CloserSpacing.md)
|
|
.background(Color.closerSurface)
|
|
.clipShape(RoundedRectangle(cornerRadius: CloserRadius.xlarge, style: .continuous))
|
|
.closerShadow(level: .small)
|
|
}
|
|
}
|
|
|
|
// MARK: - Question Answer
|
|
|
|
struct QuestionAnswerView: View {
|
|
let question: Question
|
|
let coupleId: String
|
|
let onAnswered: () -> Void
|
|
@StateObject private var viewModel = AnswerRevealViewModel()
|
|
@State private var textAnswer = ""
|
|
@State private var selectedOptions: Set<String> = []
|
|
@State private var scaleValue: Double = 5
|
|
@State private var isSubmitting = false
|
|
@State private var submitError: String?
|
|
|
|
var body: some View {
|
|
VStack(spacing: CloserSpacing.lg) {
|
|
switch question.type {
|
|
case "text":
|
|
TextEditor(text: $textAnswer)
|
|
.frame(minHeight: 120)
|
|
.padding(8)
|
|
.background(Color.closerBackground)
|
|
.cornerRadius(CloserRadius.medium)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: CloserRadius.medium)
|
|
.stroke(Color.closerDivider)
|
|
)
|
|
|
|
Button(action: submitAnswer) {
|
|
if isSubmitting {
|
|
ProgressView().tint(.white)
|
|
} else {
|
|
Text("Submit Answer")
|
|
}
|
|
}
|
|
.buttonStyle(PrimaryButtonStyle(isDisabled: isSubmitting || textAnswer.isEmpty))
|
|
.disabled(isSubmitting || textAnswer.isEmpty)
|
|
|
|
case "multiple_choice":
|
|
if let options = question.options {
|
|
ForEach(options, id: \.self) { option in
|
|
Button(action: {
|
|
selectedOptions = [option]
|
|
}) {
|
|
HStack {
|
|
Image(systemName: selectedOptions.contains(option) ? "circle.fill" : "circle")
|
|
.foregroundColor(.closerPrimary)
|
|
Text(option)
|
|
.foregroundColor(.closerText)
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
.background(selectedOptions.contains(option) ? Color.closerPrimary.opacity(0.1) : Color.closerBackground)
|
|
.cornerRadius(CloserRadius.medium)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: CloserRadius.medium)
|
|
.stroke(selectedOptions.contains(option) ? Color.closerPrimary : Color.closerDivider)
|
|
)
|
|
}
|
|
}
|
|
|
|
Button(action: submitAnswer) {
|
|
if isSubmitting {
|
|
ProgressView().tint(.white)
|
|
} else {
|
|
Text("Submit Answer")
|
|
}
|
|
}
|
|
.buttonStyle(PrimaryButtonStyle(isDisabled: isSubmitting || selectedOptions.isEmpty))
|
|
.disabled(isSubmitting || selectedOptions.isEmpty)
|
|
}
|
|
|
|
case "scale":
|
|
VStack(spacing: CloserSpacing.sm) {
|
|
Text("\(Int(scaleValue))")
|
|
.font(CloserFont.largeTitle)
|
|
.foregroundColor(.closerPrimary)
|
|
|
|
Slider(value: $scaleValue, in: 1...10, step: 1)
|
|
.tint(.closerPrimary)
|
|
|
|
HStack {
|
|
Text("1").font(CloserFont.caption).foregroundColor(.closerTextSecondary)
|
|
Spacer()
|
|
Text("10").font(CloserFont.caption).foregroundColor(.closerTextSecondary)
|
|
}
|
|
}
|
|
|
|
Button(action: submitAnswer) {
|
|
if isSubmitting {
|
|
ProgressView().tint(.white)
|
|
} else {
|
|
Text("Submit Answer")
|
|
}
|
|
}
|
|
.buttonStyle(PrimaryButtonStyle(isDisabled: isSubmitting))
|
|
.disabled(isSubmitting)
|
|
|
|
default:
|
|
Text("Unsupported question type")
|
|
.font(CloserFont.callout)
|
|
.foregroundColor(.closerTextSecondary)
|
|
if let submitError {
|
|
Text(submitError)
|
|
.font(CloserFont.caption)
|
|
.foregroundColor(.closerDanger)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func submitAnswer() {
|
|
isSubmitting = true
|
|
submitError = nil
|
|
Task {
|
|
do {
|
|
try await viewModel.submitAnswer(
|
|
coupleId: coupleId,
|
|
questionId: question.id,
|
|
answerText: answerTextForCurrentType,
|
|
answerType: question.type
|
|
)
|
|
isSubmitting = false
|
|
onAnswered()
|
|
} catch {
|
|
isSubmitting = false
|
|
submitError = (error as? AnswerError)?.localizedDescription ?? error.localizedDescription
|
|
}
|
|
}
|
|
}
|
|
|
|
private var answerTextForCurrentType: String {
|
|
switch question.type {
|
|
case "text":
|
|
return textAnswer
|
|
case "multiple_choice":
|
|
return selectedOptions.sorted().joined(separator: ",")
|
|
case "scale":
|
|
return String(Int(scaleValue))
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Answer Reveal
|
|
|
|
struct AnswerRevealView: View {
|
|
let questionId: String
|
|
let coupleId: String
|
|
@EnvironmentObject var appState: AppState
|
|
@StateObject private var viewModel = AnswerRevealViewModel()
|
|
@State private var partnerAnswer: String?
|
|
@State private var isLoading = true
|
|
@State private var showCreateInvite = false
|
|
|
|
private var isPaired: Bool {
|
|
appState.currentUser?.coupleId != nil
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: CloserSpacing.xxl) {
|
|
if !isPaired {
|
|
EmptyStateView(
|
|
icon: "lock.fill",
|
|
title: "Connect your partner",
|
|
message: "Invite your partner to unlock shared reveals and private answers you can open together.",
|
|
action: (title: "Invite partner", handler: { showCreateInvite = true })
|
|
)
|
|
} else if isLoading {
|
|
LoadingView(message: "Loading answer...")
|
|
} else if let answer = partnerAnswer {
|
|
Image(systemName: "heart.fill")
|
|
.font(.system(size: 64))
|
|
.foregroundColor(.closerDanger)
|
|
|
|
Text("Partner's Answer")
|
|
.font(CloserFont.title2)
|
|
.foregroundColor(.closerText)
|
|
|
|
Text(answer)
|
|
.font(CloserFont.body)
|
|
.foregroundColor(.closerText)
|
|
.multilineTextAlignment(.center)
|
|
.padding(CloserSpacing.xl)
|
|
.closerCard()
|
|
|
|
Text("This answer is end-to-end encrypted and can only be seen by you and your partner.")
|
|
.font(CloserFont.footnote)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
} else {
|
|
EmptyStateView(
|
|
icon: "eye.slash.fill",
|
|
title: "Not Yet Available",
|
|
message: viewModel.legacyWarning ?? "Your partner hasn't answered yet, or the answer hasn't been revealed."
|
|
)
|
|
}
|
|
}
|
|
.closerPadding()
|
|
.background(Color.closerBackground)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.fullScreenCover(isPresented: $showCreateInvite) {
|
|
CreateInviteView()
|
|
.environmentObject(appState)
|
|
}
|
|
.task {
|
|
await viewModel.loadPartnerAnswer(coupleId: coupleId, questionId: questionId)
|
|
isLoading = false
|
|
partnerAnswer = viewModel.partnerAnswer
|
|
}
|
|
.onChange(of: viewModel.partnerAnswer) { oldValue, newValue in
|
|
partnerAnswer = newValue
|
|
}
|
|
.onChange(of: viewModel.isLoading) { oldValue, newValue in
|
|
isLoading = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Answer History
|
|
|
|
struct AnswerHistoryView: View {
|
|
@EnvironmentObject var appState: AppState
|
|
@State private var answers: [Answer] = []
|
|
@State private var isLoading = true
|
|
@State private var showCreateInvite = false
|
|
|
|
private var isPaired: Bool {
|
|
appState.currentUser?.coupleId != nil
|
|
}
|
|
|
|
private var emptyAction: (title: String, handler: () -> Void)? {
|
|
isPaired ? nil : (title: "Invite partner", handler: { showCreateInvite = true })
|
|
}
|
|
|
|
var body: some View {
|
|
List {
|
|
if isLoading {
|
|
ProgressView()
|
|
.frame(maxWidth: .infinity)
|
|
} else if answers.isEmpty {
|
|
EmptyStateView(
|
|
icon: "clock.arrow.circlepath",
|
|
title: isPaired ? "No Answers Yet" : "Connect your partner",
|
|
message: isPaired
|
|
? "Your answer history will appear here."
|
|
: "Invite your partner to unlock shared reveals and build your private answer history together.",
|
|
illustrationName: "illustration-couple-history",
|
|
action: emptyAction
|
|
)
|
|
.listRowBackground(Color.clear)
|
|
} else {
|
|
ForEach(answers) { answer in
|
|
if isPaired {
|
|
NavigationLink {
|
|
if let coupleId = appState.currentCouple?.id {
|
|
AnswerRevealView(questionId: answer.questionId, coupleId: coupleId)
|
|
}
|
|
} label: {
|
|
AnswerHistoryRow(answer: answer)
|
|
}
|
|
} else {
|
|
Button {
|
|
showCreateInvite = true
|
|
} label: {
|
|
AnswerHistoryRow(answer: answer)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.insetGrouped)
|
|
.background(Color.closerBackground)
|
|
.navigationTitle("Answer History")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.fullScreenCover(isPresented: $showCreateInvite) {
|
|
CreateInviteView()
|
|
.environmentObject(appState)
|
|
}
|
|
.task {
|
|
// Load answers
|
|
try? await Task.sleep(nanoseconds: 500_000_000)
|
|
isLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct AnswerHistoryRow: View {
|
|
let answer: Answer
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(answer.answerText)
|
|
.font(CloserFont.body)
|
|
.foregroundColor(.closerText)
|
|
Text(answer.createdAt, style: .date)
|
|
.font(CloserFont.caption)
|
|
.foregroundColor(.closerTextSecondary)
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
|
|
// MARK: - Question Pack Library
|
|
|
|
struct QuestionPackLibraryView: View {
|
|
@State private var packs: [QuestionPack] = []
|
|
@State private var isLoading = true
|
|
@State private var selectedPack: QuestionPack?
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: CloserSpacing.lg) {
|
|
Text("Question Packs")
|
|
.font(CloserFont.title1)
|
|
.foregroundColor(.closerText)
|
|
.padding(.horizontal)
|
|
|
|
if isLoading {
|
|
LoadingView(message: "Loading packs...")
|
|
} else if packs.isEmpty {
|
|
EmptyStateView(
|
|
icon: "star.slash",
|
|
title: "No Packs Yet",
|
|
message: "Question packs will appear here."
|
|
)
|
|
} else {
|
|
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: CloserSpacing.md) {
|
|
ForEach(packs) { pack in
|
|
NavigationLink {
|
|
QuestionCategoryView(categoryId: pack.id)
|
|
} label: {
|
|
PackCard(pack: pack)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
}
|
|
.padding(.vertical)
|
|
}
|
|
.background(Color.closerBackground)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.task {
|
|
try? await Task.sleep(nanoseconds: 500_000_000)
|
|
packs = samplePacks
|
|
isLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PackCard: View {
|
|
let pack: QuestionPack
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: CloserSpacing.sm) {
|
|
Image(packArtworkName(pack.id))
|
|
.resizable()
|
|
.scaledToFill()
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: 82)
|
|
.clipShape(RoundedRectangle(cornerRadius: CloserRadius.medium, style: .continuous))
|
|
|
|
CategoryGlyph(name: pack.name, color: .closerPrimary)
|
|
|
|
Text(pack.name)
|
|
.font(CloserFont.headline)
|
|
.foregroundColor(.closerText)
|
|
.lineLimit(2)
|
|
|
|
Text(pack.description ?? "")
|
|
.font(CloserFont.caption)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.lineLimit(2)
|
|
|
|
if pack.isPremium {
|
|
PremiumBadge()
|
|
}
|
|
}
|
|
.padding(CloserSpacing.md)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.closerCard()
|
|
}
|
|
}
|
|
|
|
private func packArtworkName(_ packId: String) -> String {
|
|
switch packId {
|
|
case "communication":
|
|
return "pack-art-communication"
|
|
case "trust", "boundaries", "conflict", "conflict_repair", "rebuilding_trust":
|
|
return "pack-art-trust-repair"
|
|
case "emotional_intimacy", "gratitude", "couple_intimacy":
|
|
return "pack-art-intimacy"
|
|
case "fun", "date_night", "quality_time":
|
|
return "pack-art-fun-date"
|
|
case "future", "values":
|
|
return "pack-art-future-goals"
|
|
case "home_life", "stress":
|
|
return "pack-art-home-life"
|
|
case "money":
|
|
return "pack-art-money-values"
|
|
case "marriage", "parenting":
|
|
return "pack-art-family-commitment"
|
|
case "sex_and_desire", "sexual_preferences", "physical_intimacy":
|
|
return "pack-art-desire"
|
|
case "difficult_conversations":
|
|
return "pack-art-deep-reflection"
|
|
default:
|
|
return "pack-art-deep-reflection"
|
|
}
|
|
}
|
|
|
|
// MARK: - Question Category
|
|
|
|
struct QuestionCategoryView: View {
|
|
let categoryId: String
|
|
@State private var questions: [Question] = []
|
|
@State private var showComposer = false
|
|
|
|
var body: some View {
|
|
List {
|
|
if questions.isEmpty {
|
|
EmptyStateView(
|
|
icon: "questionmark.bubble",
|
|
title: "No Questions",
|
|
message: "This pack has no questions yet."
|
|
)
|
|
.listRowBackground(Color.clear)
|
|
} else {
|
|
ForEach(questions) { question in
|
|
NavigationLink {
|
|
QuestionThreadView(coupleId: "", questionId: question.id)
|
|
} label: {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(question.text)
|
|
.font(CloserFont.body)
|
|
.foregroundColor(.closerText)
|
|
Text(question.type.replacingOccurrences(of: "_", with: " ").capitalized)
|
|
.font(CloserFont.caption)
|
|
.foregroundColor(.closerTextSecondary)
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.insetGrouped)
|
|
.background(Color.closerBackground)
|
|
.navigationTitle("Questions")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .primaryAction) {
|
|
Button(action: { showComposer = true }) {
|
|
Image(systemName: "plus")
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $showComposer) {
|
|
QuestionComposerView()
|
|
}
|
|
.task {
|
|
questions = sampleQuestions
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Question Composer
|
|
|
|
struct QuestionComposerView: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var questionText = ""
|
|
@State private var selectedType = "text"
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section("Question") {
|
|
TextField("Write your question...", text: $questionText, axis: .vertical)
|
|
.lineLimit(3...6)
|
|
}
|
|
|
|
Section("Type") {
|
|
Picker("Type", selection: $selectedType) {
|
|
Text("Text").tag("text")
|
|
Text("Multiple Choice").tag("multiple_choice")
|
|
Text("Scale").tag("scale")
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("New Question")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Cancel") { dismiss() }
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button("Send") { dismiss() }
|
|
.disabled(questionText.isEmpty)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Question Thread
|
|
|
|
struct QuestionThreadView: View {
|
|
let coupleId: String
|
|
let questionId: String
|
|
@State private var messages: [QuestionMessage] = []
|
|
|
|
var body: some View {
|
|
VStack {
|
|
if messages.isEmpty {
|
|
EmptyStateView(
|
|
icon: "bubble.left.and.bubble.right",
|
|
title: "No Messages Yet",
|
|
message: "Start a conversation about this question."
|
|
)
|
|
} else {
|
|
List(messages) { message in
|
|
Text(message.text)
|
|
.font(CloserFont.body)
|
|
}
|
|
}
|
|
}
|
|
.background(Color.closerBackground)
|
|
.navigationTitle("Discussion")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
}
|
|
|
|
// MARK: - Sample Data
|
|
|
|
// Fallback sample data. IDs must match real Android seed entries so that
|
|
// navigation to category/pack/question threads references actual data.
|
|
let bundledQuestions: [Question] = [
|
|
Question(id: "communication_001", text: "What is one small thing I do that helps you feel heard?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: "communication", packId: nil),
|
|
Question(id: "communication_002", text: "When do you feel it is easiest to talk to me?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: "communication", packId: nil),
|
|
Question(id: "communication_151", text: "When you are upset, what helps most first?", type: "single_choice", options: ["Comfort", "Space", "Advice", "Distraction"], scaleMin: nil, scaleMax: nil, categoryId: "communication", packId: nil),
|
|
Question(id: "date_night_001", text: "What would make a simple dinner feel fun for both of us?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: "date_night", packId: nil),
|
|
]
|
|
|
|
let sampleQuestions: [Question] = [
|
|
Question(id: "fun_001", text: "What is one thing we do together that always makes you smile?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: "fun", packId: nil),
|
|
Question(id: "fun_002", text: "What is one silly memory of us that you still love?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: "fun", packId: nil),
|
|
Question(id: "values_001", text: "What is one value that quietly guides your life?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: "values", packId: nil),
|
|
Question(id: "trust_001", text: "What makes it easy for you to trust someone?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: "trust", packId: nil),
|
|
]
|
|
|
|
let samplePacks: [QuestionPack] = [
|
|
QuestionPack(id: "communication", name: "Communication", description: "Questions about listening, expressing needs, understanding each other, and talking clearly.", categories: nil, isPremium: false),
|
|
QuestionPack(id: "fun", name: "Fun & Playful", description: "Lighthearted questions about playfulness, laughter, and shared activities.", categories: nil, isPremium: false),
|
|
QuestionPack(id: "date_night", name: "Date Night", description: "Questions designed for dates, meals, walks, or quiet time together.", categories: nil, isPremium: true),
|
|
QuestionPack(id: "trust", name: "Trust", description: "Questions about trust, repair, and rebuilding.", categories: nil, isPremium: true),
|
|
]
|