# Create a new branch from maingit checkout maingit pull origin maingit checkout -b feature/your-feature-name# Or for bug fixesgit checkout -b fix/issue-description
feat: add real-time chat presence indicatorsImplements green/gray dots next to user avatars in chat to showonline status. Uses Supabase Realtime presence tracking.Closes #123---fix: prevent navigation stack overflow in modal sheetsThe NavigationRouter now properly dismisses modals before pushingnew destinations, preventing the stack overflow that occurred whennavigating from profile sheets.Fixes #456---refactor: extract accommodation filters into reusable component
## SummaryBrief description of what this PR does## Changes- Added accommodation price range filter UI- Connected filter to AccommodationV2Manager- Updated AccommodationV2FilterView with new slider component## Testing- [x] Tested on iPhone 15 Pro simulator (iOS 17.5)- [x] Tested on physical iPhone 13 (iOS 17.4)- [x] Verified filter persists across app restarts- [x] Tested with various price ranges## Screenshots[Add screenshots or screen recordings]## Related IssuesCloses #123Related to #456## Notes- Considered using a segmented control but slider provides better UX- Filter state is saved to UserDefaults for persistence
While test coverage is currently minimal, new critical business logic should include tests:
import XCTest@testable import Chapterfinal class MatchManagerTests: XCTestCase { var sut: MatchManager! override func setUp() { super.setUp() sut = MatchManager() } override func tearDown() { sut = nil super.tearDown() } func testCalculateMatchScore_WithPerfectMatch_Returns100() { // Given let user = GroupUser(/* perfect match data */) let university = UniversityV2(/* matching requirements */) // When let score = sut.calculateMatchScore(user: user, university: university) // Then XCTAssertEqual(score, 100.0, accuracy: 0.01) } func testCalculateMatchScore_WithNoMatch_Returns0() { // Given let user = GroupUser(/* no match data */) let university = UniversityV2(/* different requirements */) // When let score = sut.calculateMatchScore(user: user, university: university) // Then XCTAssertEqual(score, 0.0, accuracy: 0.01) }}
// ❌ Don't bypass NavigationRouterstruct MyView: View { @State private var path = NavigationPath() var body: some View { NavigationStack(path: $path) { // This creates a separate navigation stack! } }}// ✅ Use NavigationRouter.sharedstruct MyView: View { var body: some View { Button("Go to Profile") { NavigationRouter.shared.navigate(to: .userProfile(user)) } }}
// ❌ Don't add @Published to AppContainer for every serviceclass AppContainer: ObservableObject { @Published var someManager: SomeManager // This triggers re-render of ENTIRE APP!}// ✅ Let services manage their own @Published stateclass AppContainer: ObservableObject { var someManager: SomeManager // Not @Published}// Services publish their own updatesclass SomeManager: ObservableObject { @Published var data: [Item] = [] // Only views observing this re-render}
// ❌ UI updates from background threadfunc loadData() async { let data = try await fetchFromAPI() self.items = data // ⚠️ Publishing changes from background thread!}// ✅ Ensure UI updates on main threadfunc loadData() async { let data = try await fetchFromAPI() await MainActor.run { self.items = data }}// Or mark entire class@MainActorclass MyViewModel: ObservableObject { // All methods run on MainActor}
// Add accessibility labelsImage(systemName: "heart.fill") .accessibilityLabel("Favorite")Button(action: deleteItem) { Image(systemName: "trash")}.accessibilityLabel("Delete item").accessibilityHint("Removes this item from your list")// Support Dynamic TypeText("Hello") .font(.body) // Scales with user's text size preference// Provide alternative text for imagesAsyncImage(url: imageURL) .accessibilityLabel("University campus photo")