diff --git a/nahbar/nahbar.xcodeproj/project.xcworkspace/xcuserdata/sven.xcuserdatad/UserInterfaceState.xcuserstate b/nahbar/nahbar.xcodeproj/project.xcworkspace/xcuserdata/sven.xcuserdatad/UserInterfaceState.xcuserstate index 451fda2..5af515e 100644 Binary files a/nahbar/nahbar.xcodeproj/project.xcworkspace/xcuserdata/sven.xcuserdatad/UserInterfaceState.xcuserstate and b/nahbar/nahbar.xcodeproj/project.xcworkspace/xcuserdata/sven.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/nahbar/nahbar/IchView.swift b/nahbar/nahbar/IchView.swift index 5749a17..7ec2263 100644 --- a/nahbar/nahbar/IchView.swift +++ b/nahbar/nahbar/IchView.swift @@ -189,8 +189,34 @@ struct IchView: View { } // Vorlieben - if !profileStore.likes.isEmpty || !profileStore.dislikes.isEmpty { - SectionHeader(title: "Vorlieben", icon: "heart") + SectionHeader(title: "Vorlieben", icon: "heart") + if profileStore.likes.isEmpty && profileStore.dislikes.isEmpty { + // Nudge: fehlende Vorlieben + Button { showingEdit = true } label: { + HStack(spacing: 12) { + Image(systemName: "sparkles") + .font(.system(size: 14)) + .foregroundStyle(theme.accent) + Text("Uns fehlt noch was – wir würden gerne mehr von dir erfahren.") + .font(.system(size: 14)) + .foregroundStyle(theme.contentSecondary) + .multilineTextAlignment(.leading) + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(theme.contentTertiary) + } + .padding(.horizontal, 16) + .padding(.vertical, 14) + .background(theme.accent.opacity(0.06)) + .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) + .overlay( + RoundedRectangle(cornerRadius: theme.radiusCard) + .strokeBorder(theme.accent.opacity(0.18), lineWidth: 1) + ) + } + .buttonStyle(.plain) + } else { VStack(spacing: 0) { if !profileStore.likes.isEmpty { preferenceRow(label: "Mag ich", text: profileStore.likes, color: .green) diff --git a/nahbar/nahbar/Localizable.xcstrings b/nahbar/nahbar/Localizable.xcstrings index 2664a2f..c752374 100644 --- a/nahbar/nahbar/Localizable.xcstrings +++ b/nahbar/nahbar/Localizable.xcstrings @@ -3979,6 +3979,9 @@ } } } + }, + "Uns fehlt noch was – wir würden gerne mehr von dir erfahren." : { + }, "Unwohl" : { "comment" : "RatingQuestion – negative pole for comfort during meeting", diff --git a/nahbar/nahbar/PeopleListView.swift b/nahbar/nahbar/PeopleListView.swift index bc83c8f..ed80976 100644 --- a/nahbar/nahbar/PeopleListView.swift +++ b/nahbar/nahbar/PeopleListView.swift @@ -11,6 +11,7 @@ struct PeopleListView: View { @State private var selectedTag: PersonTag? = nil @State private var showingAddPerson = false @State private var showingPaywall = false + @State private var selectedPerson: Person? = nil private let freeContactLimit = 3 @@ -112,8 +113,11 @@ struct PeopleListView: View { ScrollView { VStack(spacing: 0) { ForEach(Array(filteredPeople.enumerated()), id: \.element.id) { index, person in - NavigationLink(destination: PersonDetailView(person: person)) { + Button { + selectedPerson = person + } label: { PersonRowView(person: person) + .contentShape(Rectangle()) } .buttonStyle(.plain) if index < filteredPeople.count - 1 { @@ -132,6 +136,9 @@ struct PeopleListView: View { } .background(theme.backgroundPrimary.ignoresSafeArea()) .navigationBarHidden(true) + .navigationDestination(item: $selectedPerson) { person in + PersonDetailView(person: person) + } } .sheet(isPresented: $showingAddPerson) { AddPersonView() diff --git a/nahbar/nahbar/PersonDetailView.swift b/nahbar/nahbar/PersonDetailView.swift index 0edaa3c..e88166d 100644 --- a/nahbar/nahbar/PersonDetailView.swift +++ b/nahbar/nahbar/PersonDetailView.swift @@ -590,11 +590,14 @@ private struct DeletableMomentRow: View { } .background(theme.surfaceCard) .offset(x: offset) - .gesture( - DragGesture(minimumDistance: 10, coordinateSpace: .local) + // simultaneousGesture erlaubt dem übergeordneten ScrollView weiterhin zu scrollen. + // Der Winkeltest (Faktor 2.5) lässt nur klar horizontale Gesten durch. + .simultaneousGesture( + DragGesture(minimumDistance: 20, coordinateSpace: .local) .onChanged { value in let x = value.translation.width - guard abs(x) > abs(value.translation.height) * 0.6 else { return } + let y = value.translation.height + guard abs(x) > abs(y) * 2.5 else { return } if x > 0 { offset = min(x, actionWidth + 16) } else { @@ -603,6 +606,11 @@ private struct DeletableMomentRow: View { } .onEnded { value in let x = value.translation.width + let y = value.translation.height + guard abs(x) > abs(y) * 2.5 else { + withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = 0 } + return + } if x > actionWidth + 20 { // Vollständiges Rechts-Wischen: sofort togglen, zurückspringen onToggleImportant() diff --git a/nahbar/nahbarTests/UserProfileStoreTests.swift b/nahbar/nahbarTests/UserProfileStoreTests.swift index 76485d1..b16b75b 100644 --- a/nahbar/nahbarTests/UserProfileStoreTests.swift +++ b/nahbar/nahbarTests/UserProfileStoreTests.swift @@ -185,3 +185,62 @@ struct UserProfileStoreNewFieldsTests { #expect(Set(options).count == options.count) } } + +// MARK: - Vorlieben-Nudge Anzeigelogik + +@Suite("IchView – Vorlieben-Nudge Sichtbarkeit") +struct VorliebenNudgeTests { + + // Spiegelt die Bedingung in IchView.infoSection wider: + // showNudge = likes.isEmpty && dislikes.isEmpty + // showContent = !likes.isEmpty || !dislikes.isEmpty + // infoSection nur sichtbar wenn !isEmpty (getrennt getestet) + + private func showNudge(likes: String, dislikes: String) -> Bool { + likes.isEmpty && dislikes.isEmpty + } + + @Test("Beide leer → Nudge wird angezeigt") + func bothEmptyShowsNudge() { + #expect(showNudge(likes: "", dislikes: "")) + } + + @Test("Nur likes gesetzt → kein Nudge") + func onlyLikesSetHidesNudge() { + #expect(!showNudge(likes: "Kaffee", dislikes: "")) + } + + @Test("Nur dislikes gesetzt → kein Nudge") + func onlyDislikesSetHidesNudge() { + #expect(!showNudge(likes: "", dislikes: "Lärm")) + } + + @Test("Beide gesetzt → kein Nudge") + func bothSetHidesNudge() { + #expect(!showNudge(likes: "Kaffee", dislikes: "Lärm")) + } + + @Test("Nudge und Content schließen sich gegenseitig aus") + func nudgeAndContentAreMutuallyExclusive() { + let cases: [(likes: String, dislikes: String)] = [ + ("", ""), + ("Kaffee", ""), + ("", "Lärm"), + ("Kaffee", "Lärm"), + ] + for c in cases { + let nudge = showNudge(likes: c.likes, dislikes: c.dislikes) + let content = !c.likes.isEmpty || !c.dislikes.isEmpty + // Genau einer von beiden ist wahr, nie beide gleichzeitig + #expect(nudge != content || (nudge == false && content == false), + "likes='\(c.likes)' dislikes='\(c.dislikes)': nudge=\(nudge) content=\(content)") + } + } + + @Test("Whitespace-only likes gilt als leer → Nudge erscheint (Store trimmt beim Speichern)") + func whitespaceOnlyLikesCountsAsEmpty() { + // Der Store trimmt Eingaben beim Speichern, daher landet nie reines Whitespace + let trimmed = " ".trimmingCharacters(in: .whitespaces) + #expect(showNudge(likes: trimmed, dislikes: "")) + } +}