diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 72c31cb6..db5c6661 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -292,6 +292,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"src/Preview Content\""; ENABLE_PREVIEWS = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/src", @@ -459,6 +460,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"src/Preview Content\""; ENABLE_PREVIEWS = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/src", diff --git a/iosApp/src/Components/AddToCalendarButton.swift b/iosApp/src/Components/AddToCalendarButton.swift index fb7ef2f4..b435a95f 100644 --- a/iosApp/src/Components/AddToCalendarButton.swift +++ b/iosApp/src/Components/AddToCalendarButton.swift @@ -43,26 +43,25 @@ struct AddToCalendarButton: View { // MARK: - Preview #Preview { + let now = ISO8601DateFormatter().string(from: Date()) + VStack(spacing: 20) { AddToCalendarButton( event: Event( id: "event-1", + organizerId: "user-1", title: "Team Meeting", description: "Q4 Planning", - organizerId: "user-1", - participants: [], - proposedSlots: [], + status: "CONFIRMED", deadline: ISO8601DateFormatter().string(from: Date()), - status: .confirmed, - finalDate: ISO8601DateFormatter().string(from: Date().addingTimeInterval(86400)), - createdAt: ISO8601DateFormatter().string(from: Date()), - updatedAt: ISO8601DateFormatter().string(from: Date()), - eventType: .teamBuilding, + createdAt: now, + updatedAt: now, + version: 1, + eventType: "TEAM_BUILDING", eventTypeCustom: nil, minParticipants: nil, maxParticipants: nil, - expectedParticipants: nil, - heroImageUrl: nil + expectedParticipants: nil ), isLoading: false, isEnabled: true, @@ -72,22 +71,19 @@ struct AddToCalendarButton: View { AddToCalendarButton( event: Event( id: "event-2", + organizerId: "user-1", title: "Team Meeting", description: "Q4 Planning", - organizerId: "user-1", - participants: [], - proposedSlots: [], + status: "POLLING", deadline: ISO8601DateFormatter().string(from: Date()), - status: .polling, - finalDate: nil, - createdAt: ISO8601DateFormatter().string(from: Date()), - updatedAt: ISO8601DateFormatter().string(from: Date()), - eventType: .teamBuilding, + createdAt: now, + updatedAt: now, + version: 1, + eventType: "TEAM_BUILDING", eventTypeCustom: nil, minParticipants: nil, maxParticipants: nil, - expectedParticipants: nil, - heroImageUrl: nil + expectedParticipants: nil ), isLoading: true, isEnabled: false, diff --git a/iosApp/src/ContentView.swift b/iosApp/src/ContentView.swift index 38a6488c..064ef4bf 100644 --- a/iosApp/src/ContentView.swift +++ b/iosApp/src/ContentView.swift @@ -117,7 +117,7 @@ struct AuthenticatedView: View { let userId: String @State private var selectedTab: WakeveTab = .home @State private var currentView: AppView = .eventList - @State private var selectedEvent: Event? + @State private var selectedEvent: Event_? // Use persistent database-backed repository instead of in-memory mock private let repository: EventRepositoryInterface = RepositoryProvider.shared.repository @State private var showEventCreationSheet = false @@ -229,10 +229,26 @@ struct AuthenticatedView: View { .foregroundColor(.secondary) case .eventDetail: - if selectedEvent != nil { - Text("Event Detail - Coming Soon") - .font(.title2) - .foregroundColor(.secondary) + if let event = selectedEvent { + EventDetailExperienceView( + event: event, + repository: repository, + onBack: { + currentView = .eventList + }, + onOpenParticipants: { + currentView = .participantManagement + }, + onOpenVote: { + currentView = .pollVoting + }, + onOpenTransport: { + currentView = .transportPlan + }, + onOpenMessages: { + selectedTab = .inbox + } + ) } case .participantManagement: @@ -242,9 +258,7 @@ struct AuthenticatedView: View { repository: repository, onParticipantsUpdated: { // Refresh the event data - if let updatedEvent = repository.getEvent(id: event.id) { - selectedEvent = updatedEvent - } + refreshSelectedEvent() }, onBack: { currentView = .eventDetail @@ -259,6 +273,7 @@ struct AuthenticatedView: View { repository: repository, participantId: userId, onVoteSubmitted: { + refreshSelectedEvent() currentView = .eventDetail }, onBack: { @@ -281,6 +296,17 @@ struct AuthenticatedView: View { } ) } + + case .transportPlan: + if let event = selectedEvent { + TransportPlanView( + event: event, + repository: repository, + onBack: { + currentView = .eventDetail + } + ) + } // MARK: - New PRD Features Navigation Cases @@ -370,6 +396,13 @@ struct AuthenticatedView: View { ExploreTabView() } } + + private func refreshSelectedEvent() { + guard let eventId = selectedEvent?.id else { return } + if let updatedEvent = repository.getEvent(id: eventId) { + selectedEvent = updatedEvent + } + } } @@ -385,6 +418,7 @@ enum AppView { case participantManagement case pollVoting case pollResults + case transportPlan // New PRD features case scenarioList case scenarioDetail @@ -403,10 +437,10 @@ enum AppView { struct EventListView: View { let repository: EventRepository - let onEventSelected: (Event) -> Void + let onEventSelected: (Event_) -> Void let onCreateEvent: () -> Void - @State private var events: [Event] = [] + @State private var events: [Event_] = [] @State private var isLoading = true var body: some View { @@ -507,7 +541,7 @@ struct EventListView: View { } struct EventCard: View { - let event: Event + let event: Event_ let onTap: () -> Void var body: some View { @@ -519,8 +553,8 @@ struct EventCard: View { .font(.system(size: 18, weight: .semibold, design: .rounded)) .foregroundColor(.primary) - if !event.description.isEmpty { - Text(event.description) + if !event.description_.isEmpty { + Text(event.description_) .font(.system(size: 14, design: .rounded)) .foregroundColor(.secondary) .lineLimit(2) @@ -600,7 +634,7 @@ struct EventCard: View { } struct EventDetailView: View { - let event: Event + let event: Event_ let repository: EventRepository let onManageParticipants: () -> Void let onVote: () -> Void @@ -647,8 +681,8 @@ struct EventDetailView: View { Spacer() } - if !event.description.isEmpty { - Text(event.description) + if !event.description_.isEmpty { + Text(event.description_) .font(.system(size: 14, design: .rounded)) .foregroundColor(.secondary) .multilineTextAlignment(.leading) diff --git a/iosApp/src/Preview/Factories/EventFactory.swift b/iosApp/src/Preview/Factories/EventFactory.swift index 88d85cb6..fcaedcc4 100644 --- a/iosApp/src/Preview/Factories/EventFactory.swift +++ b/iosApp/src/Preview/Factories/EventFactory.swift @@ -5,190 +5,133 @@ import Shared enum EventFactory { - // MARK: - ISO 8601 Helpers - - private static let iso8601: ISO8601DateFormatter = { - let f = ISO8601DateFormatter() - return f - }() - + private static let iso8601 = ISO8601DateFormatter() private static var now: String { iso8601.string(from: Date()) } private static func dateString(daysFromNow days: Int, hour: Int = 10) -> String { var components = DateComponents() components.day = days components.hour = hour - Calendar.current.component(.hour, from: Date()) - let date = Calendar.current.date(byAdding: components, to: Date())! + let date = Calendar.current.date(byAdding: components, to: Date()) ?? Date() return iso8601.string(from: date) } - // MARK: - Named Variants + private static func slot( + id: String, + start: String?, + end: String?, + timezone: String = "Europe/Paris", + timeOfDay: TimeOfDay = .specific + ) -> TimeSlot_ { + TimeSlot_(id: id, start: start, end: end, timezone: timezone, timeOfDay: timeOfDay) + } - /// Minimal draft event — no participants, no final date, no proposed slots. - static var empty: Event { - Event( + static var empty: Event_ { + make( id: "event-empty-001", title: "", description: "", - organizerId: UserFactory.organizer.id, - participants: [], - proposedSlots: [], - deadline: dateString(daysFromNow: 7), status: .draft, - finalDate: nil, - createdAt: now, - updatedAt: now, - eventType: .other, - eventTypeCustom: nil, - minParticipants: nil, - maxParticipants: nil, - expectedParticipants: nil, - heroImageUrl: nil + eventType: Shared.EventType.other ) } - /// Fully populated event — confirmed status, participants, final date, hero image. - static var complete: Event { - let participants = UserFactory.group(count: 5).map(\.id) - return Event( + static var complete: Event_ { + make( id: "event-complete-002", title: "Weekend Hiking Trip", description: "Join us for a scenic hike through the Calanques National Park followed by a picnic.", - organizerId: UserFactory.organizer.id, - participants: participants, + participants: UserFactory.group(count: 5).map(\.id), proposedSlots: sampleTimeSlots, deadline: dateString(daysFromNow: 3), status: .confirmed, finalDate: dateString(daysFromNow: 14, hour: 9), createdAt: dateString(daysFromNow: -7), - updatedAt: now, - eventType: .outdoorActivity, - eventTypeCustom: nil, - minParticipants: KotlinInt(value: 3), - maxParticipants: KotlinInt(value: 20), - expectedParticipants: KotlinInt(value: 8), + eventType: Shared.EventType.outdoorActivity, + minParticipants: 3, + maxParticipants: 20, + expectedParticipants: 8, heroImageUrl: "https://images.unsplash.com/photo-1551632811-561732d1e306?w=800" ) } - /// Event with finalDate in the past, finalized status. - static var past: Event { - let participants = UserFactory.group(count: 4).map(\.id) - return Event( + static var past: Event_ { + make( id: "event-past-003", title: "Team Offsite 2025", description: "Annual team building weekend in the Alps.", - organizerId: UserFactory.organizer.id, - participants: participants, - proposedSlots: [], + participants: UserFactory.group(count: 4).map(\.id), deadline: dateString(daysFromNow: -30), status: .finalized, finalDate: dateString(daysFromNow: -14, hour: 10), createdAt: dateString(daysFromNow: -60), updatedAt: dateString(daysFromNow: -14), - eventType: .teamBuilding, - eventTypeCustom: nil, - minParticipants: KotlinInt(value: 4), - maxParticipants: nil, - expectedParticipants: KotlinInt(value: 6), - heroImageUrl: nil + eventType: Shared.EventType.teamBuilding, + minParticipants: 4, + expectedParticipants: 6 ) } - /// Event in polling status with proposed time slots. - static var polling: Event { - let participants = UserFactory.group(count: 3).map(\.id) - return Event( + static var polling: Event_ { + make( id: "event-polling-004", title: "Birthday Dinner", description: "Celebrating Emma's birthday! Vote for the best date.", - organizerId: UserFactory.organizer.id, - participants: participants, + participants: UserFactory.group(count: 3).map(\.id), proposedSlots: [ - TimeSlot( - id: "slot-poll-1", - start: dateString(daysFromNow: 7, hour: 19), - end: dateString(daysFromNow: 7, hour: 22), - timezone: "Europe/Paris", - timeOfDay: .evening - ), - TimeSlot( - id: "slot-poll-2", - start: dateString(daysFromNow: 8, hour: 12), - end: dateString(daysFromNow: 8, hour: 14), - timezone: "Europe/Paris", - timeOfDay: .afternoon - ), - TimeSlot( - id: "slot-poll-3", - start: nil, - end: nil, - timezone: "Europe/Paris", - timeOfDay: .morning - ) + slot(id: "slot-poll-1", start: dateString(daysFromNow: 7, hour: 19), end: dateString(daysFromNow: 7, hour: 22), timeOfDay: .evening), + slot(id: "slot-poll-2", start: dateString(daysFromNow: 8, hour: 12), end: dateString(daysFromNow: 8, hour: 14), timeOfDay: .afternoon), + slot(id: "slot-poll-3", start: nil, end: nil, timeOfDay: .morning) ], deadline: dateString(daysFromNow: 5), status: .polling, - finalDate: nil, createdAt: dateString(daysFromNow: -2), - updatedAt: now, - eventType: .birthday, - eventTypeCustom: nil, - minParticipants: KotlinInt(value: 5), - maxParticipants: KotlinInt(value: 15), - expectedParticipants: KotlinInt(value: 10), - heroImageUrl: nil + eventType: Shared.EventType.birthday, + minParticipants: 5, + maxParticipants: 15, + expectedParticipants: 10 ) } - /// Event with 15+ participants for testing large participant lists. - static var withManyParticipants: Event { - let participantIds = (0..<18).map { "user-crowd-\($0)" } - return Event( + static var withManyParticipants: Event_ { + make( id: "event-crowd-005", title: "Company Summer Party", description: "End-of-summer celebration with BBQ, games, and live music at Parc Borély.", - organizerId: UserFactory.organizer.id, - participants: participantIds, - proposedSlots: [], + participants: (0..<18).map { "user-crowd-\($0)" }, deadline: dateString(daysFromNow: 10), status: .confirmed, finalDate: dateString(daysFromNow: 21, hour: 16), createdAt: dateString(daysFromNow: -14), - updatedAt: now, - eventType: .party, - eventTypeCustom: nil, - minParticipants: KotlinInt(value: 10), - maxParticipants: KotlinInt(value: 50), - expectedParticipants: KotlinInt(value: 25), + eventType: Shared.EventType.party, + minParticipants: 10, + maxParticipants: 50, + expectedParticipants: 25, heroImageUrl: "https://images.unsplash.com/photo-1533174072545-7a4b6ad7a6c3?w=800" ) } - // MARK: - Builder - - /// Create an event with sensible defaults. Override only the parameters you need. static func make( id: String = "event-\(UUID().uuidString.prefix(8))", title: String = "Test Event", description: String = "A test event for preview purposes.", organizerId: String = UserFactory.organizer.id, participants: [String] = [], - proposedSlots: [TimeSlot] = [], + proposedSlots: [TimeSlot_] = [], deadline: String? = nil, status: EventStatus = .draft, finalDate: String? = nil, createdAt: String? = nil, updatedAt: String? = nil, - eventType: Shared.EventType = .other, + eventType: Shared.EventType = Shared.EventType.other, eventTypeCustom: String? = nil, minParticipants: Int32? = nil, maxParticipants: Int32? = nil, expectedParticipants: Int32? = nil, heroImageUrl: String? = nil - ) -> Event { + ) -> Event_ { let nowString = now - return Event( + return Event_( id: id, title: title, description: description, @@ -209,25 +152,10 @@ enum EventFactory { ) } - // MARK: - Shared Time Slots - - /// Sample time slots reusable across variants. - private static var sampleTimeSlots: [TimeSlot] { + private static var sampleTimeSlots: [TimeSlot_] { [ - TimeSlot( - id: "slot-sample-1", - start: dateString(daysFromNow: 14, hour: 9), - end: dateString(daysFromNow: 14, hour: 17), - timezone: "Europe/Paris", - timeOfDay: .specific - ), - TimeSlot( - id: "slot-sample-2", - start: nil, - end: nil, - timezone: "Europe/Paris", - timeOfDay: .morning - ) + slot(id: "slot-sample-1", start: dateString(daysFromNow: 14, hour: 9), end: dateString(daysFromNow: 14, hour: 17), timeOfDay: .specific), + slot(id: "slot-sample-2", start: nil, end: nil, timeOfDay: .morning) ] } } diff --git a/iosApp/src/Services/RepositoryProvider.swift b/iosApp/src/Services/RepositoryProvider.swift index 2ffe1975..620f087e 100644 --- a/iosApp/src/Services/RepositoryProvider.swift +++ b/iosApp/src/Services/RepositoryProvider.swift @@ -25,7 +25,7 @@ class RepositoryProvider { // Create sync dependencies let networkDetector = KtorSyncHttpClientKt.createNetworkStatusDetector() let httpClient = KtorSyncHttpClientKt.createSyncHttpClient(baseUrl: "http://localhost:8080") - let userRepository = UserRepository(db: database) + let userRepository = UserRepository_(db: database) let eventRepository = DatabaseEventRepository(db: database, syncManager: nil) let metrics = InMemorySyncMetrics() let alertManager = LoggingSyncAlertManager() diff --git a/iosApp/src/ViewModels/EventDetailViewModel.swift b/iosApp/src/ViewModels/EventDetailViewModel.swift index a98a786b..a9bcc735 100644 --- a/iosApp/src/ViewModels/EventDetailViewModel.swift +++ b/iosApp/src/ViewModels/EventDetailViewModel.swift @@ -27,7 +27,7 @@ class EventDetailViewModel: StateMachineViewModel< // MARK: - Published Properties /// The currently selected event (filtered from state.events) - @Published var selectedEvent: Event? + @Published var selectedEvent: Event_? // MARK: - Private Properties @@ -86,7 +86,7 @@ class EventDetailViewModel: StateMachineViewModel< } /// Update the event - func updateEvent(_ event: Event) { + func updateEvent(_ event: Event_) { dispatch(EventManagementContractIntentUpdateEvent(event: event)) } diff --git a/iosApp/src/Views/CreateEventSheet.swift b/iosApp/src/Views/CreateEventSheet.swift index eb1adeee..149edf1f 100644 --- a/iosApp/src/Views/CreateEventSheet.swift +++ b/iosApp/src/Views/CreateEventSheet.swift @@ -36,7 +36,7 @@ struct CreateEventSheet: View { // Preview state @State private var showingPreview = false - var onEventCreated: (Event) -> Void = { _ in } + var onEventCreated: (Event_) -> Void = { _ in } var body: some View { ZStack(alignment: .top) { @@ -485,7 +485,7 @@ struct CreateEventSheet: View { } private func createEvent() { - let event = Event( + let event = Event_( id: "event-\(Int(Date().timeIntervalSince1970 * 1000))", title: title, description: description, diff --git a/iosApp/src/Views/EventDetailExperienceView.swift b/iosApp/src/Views/EventDetailExperienceView.swift new file mode 100644 index 00000000..1c60bba8 --- /dev/null +++ b/iosApp/src/Views/EventDetailExperienceView.swift @@ -0,0 +1,397 @@ +import SwiftUI +import Shared + +struct EventDetailExperienceView: View { + let event: Event_ + let repository: EventRepositoryInterface + let onBack: () -> Void + let onOpenParticipants: () -> Void + let onOpenVote: () -> Void + let onOpenTransport: () -> Void + let onOpenMessages: () -> Void + + @State private var participants: [String] = [] + @State private var messageText = "" + + var body: some View { + ZStack { + Color(.systemBackground).ignoresSafeArea() + + ScrollView(showsIndicators: false) { + VStack(spacing: 16) { + header + coverCard + eventMetaCard + quickActions + aboutCard + messageComposer + } + .padding(.horizontal, 16) + .padding(.bottom, 24) + } + } + .onAppear(perform: loadParticipants) + } + + private var header: some View { + HStack { + Button(action: onBack) { + Image(systemName: "chevron.left") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.primary) + .frame(width: 36, height: 36) + .background(Color(.tertiarySystemFill)) + .clipShape(Circle()) + } + + Spacer() + + Text(event.title) + .font(.system(size: 20, weight: .bold)) + .lineLimit(1) + + Spacer() + + Menu { + Button("Messages", action: onOpenMessages) + Button("Participants", action: onOpenParticipants) + } label: { + Image(systemName: "ellipsis") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.primary) + .frame(width: 36, height: 36) + .background(Color(.tertiarySystemFill)) + .clipShape(Circle()) + } + } + .padding(.top, 8) + } + + private var coverCard: some View { + ZStack(alignment: .bottomLeading) { + LinearGradient( + colors: [Color.wakevePrimary, Color.wakeveAccent], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + + VStack(alignment: .leading, spacing: 8) { + Text(event.title) + .font(.system(size: 28, weight: .bold)) + .foregroundColor(.white) + } + .padding(16) + } + .frame(height: 180) + .clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous)) + } + + private var eventMetaCard: some View { + VStack(alignment: .leading, spacing: 12) { + Label(primarySlotDate, systemImage: "calendar") + .font(.subheadline.weight(.semibold)) + + Label(primarySlotLocation, systemImage: "mappin.and.ellipse") + .font(.subheadline) + .foregroundStyle(.secondary) + + HStack(spacing: 8) { + ForEach(participants.prefix(7), id: \.self) { participant in + AvatarCircle(label: participantInitials(participant)) + } + + if participants.count > 7 { + Text("+\(participants.count - 7)") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(16) + .glassCard(cornerRadius: 20) + } + + private var quickActions: some View { + HStack(spacing: 10) { + ActionTile(title: "Details", icon: "info.circle", action: { }) + ActionTile(title: "Participants", icon: "person.2", action: onOpenParticipants) + ActionTile( + title: voteActionTitle, + icon: voteActionIcon, + isEnabled: canOpenVoteAction, + action: onOpenVote + ) + ActionTile(title: "Transport", icon: "car", action: onOpenTransport) + } + } + + private var aboutCard: some View { + VStack(alignment: .leading, spacing: 8) { + Text("À propos") + .font(.headline) + + if event.description_.isEmpty { + Text("Pas de description pour le moment.") + .font(.subheadline) + .foregroundStyle(.secondary) + } else { + Text(event.description_) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(16) + .glassCard(cornerRadius: 20) + } + + private var messageComposer: some View { + HStack(spacing: 10) { + TextField("Écrire un message...", text: $messageText) + .textFieldStyle(.plain) + .padding(.horizontal, 14) + .padding(.vertical, 12) + .background(Color(.tertiarySystemFill)) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + + Button(action: onOpenMessages) { + Image(systemName: "paperplane") + .font(.system(size: 18, weight: .semibold)) + .foregroundStyle(.white) + .frame(width: 44, height: 44) + .background(Color.wakevePrimary) + .clipShape(Circle()) + } + } + .padding(12) + .glassCard(cornerRadius: 20) + } + + private var primarySlotDate: String { + guard let slot = event.proposedSlots.first else { return "Date à confirmer" } + return formatDateTime(slot.start) ?? "Date à confirmer" + } + + private var canVote: Bool { + event.status == .polling && !event.proposedSlots.isEmpty + } + + private var canViewResults: Bool { + event.status != .draft && !event.proposedSlots.isEmpty + } + + private var canOpenVoteAction: Bool { + canVote || canViewResults + } + + private var voteActionTitle: String { + canVote ? "Vote" : "Results" + } + + private var voteActionIcon: String { + canVote ? "checkmark.circle" : "chart.bar" + } + + private var primarySlotLocation: String { + return "Lieu à confirmer" + } + + private func loadParticipants() { + participants = repository.getParticipants(eventId: event.id) ?? [] + } + + private func participantInitials(_ email: String) -> String { + let name = email.split(separator: "@").first.map(String.init) ?? email + let chunks = name.split(separator: ".") + if chunks.count >= 2 { + return String(chunks[0].prefix(1) + chunks[1].prefix(1)).uppercased() + } + return String(name.prefix(2)).uppercased() + } + + private func formatDateTime(_ value: String?) -> String? { + guard let value, let date = ISO8601DateFormatter().date(from: value) else { return nil } + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "fr_FR") + formatter.dateFormat = "EEEE d MMM • HH:mm" + return formatter.string(from: date).capitalized + } +} + +struct TransportPlanView: View { + let event: Event_ + let repository: EventRepositoryInterface + let onBack: () -> Void + + @State private var selectedMode: TransportMode = .trip + @State private var participants: [String] = [] + + var body: some View { + ZStack { + Color(.systemBackground).ignoresSafeArea() + + VStack(spacing: 16) { + HStack { + Button(action: onBack) { + Image(systemName: "chevron.left") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.primary) + .frame(width: 36, height: 36) + .background(Color(.tertiarySystemFill)) + .clipShape(Circle()) + } + + Spacer() + + Text("Transport") + .font(.system(size: 24, weight: .bold)) + + Spacer() + + Image(systemName: "ellipsis") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.secondary) + .frame(width: 36, height: 36) + } + .padding(.horizontal, 16) + .padding(.top, 8) + + Picker("Transport mode", selection: $selectedMode) { + Text("Trajet").tag(TransportMode.trip) + Text("Covoiturage").tag(TransportMode.carpool) + } + .pickerStyle(.segmented) + .padding(.horizontal, 16) + + VStack(spacing: 14) { + RoundedRectangle(cornerRadius: 18, style: .continuous) + .fill(Color(.tertiarySystemFill)) + .overlay( + Image(systemName: "point.topleft.down.curvedto.point.bottomright.up") + .font(.system(size: 34, weight: .medium)) + .foregroundStyle(.secondary) + ) + .frame(height: 190) + + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("Départ") + .font(.caption) + .foregroundStyle(.secondary) + Text(slotStartTime) + .font(.title3.weight(.bold)) + } + + Spacer() + + VStack(alignment: .trailing, spacing: 4) { + Text("Arrivée") + .font(.caption) + .foregroundStyle(.secondary) + Text(slotEndTime) + .font(.title3.weight(.bold)) + } + } + + HStack(spacing: 8) { + ForEach(participants.prefix(6), id: \.self) { participant in + AvatarCircle(label: participantInitials(participant)) + } + if participants.count > 6 { + Text("+\(participants.count - 6)") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(16) + .glassCard(cornerRadius: 20) + .padding(.horizontal, 16) + + Button { + } label: { + Text("Je participe") + .font(.system(size: 18, weight: .semibold)) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .frame(height: 52) + .background(Color.wakevePrimary) + .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) + } + .padding(.horizontal, 16) + + Spacer() + } + } + .onAppear { + participants = repository.getParticipants(eventId: event.id) ?? [] + } + } + + private var slotStartTime: String { + timeLabel(event.proposedSlots.first?.start, fallback: "17:30") + } + + private var slotEndTime: String { + timeLabel(event.proposedSlots.first?.end, fallback: "18:00") + } + + private func participantInitials(_ email: String) -> String { + String(email.prefix(2)).uppercased() + } + + private func timeLabel(_ value: String?, fallback: String) -> String { + guard let value, let date = ISO8601DateFormatter().date(from: value) else { + return fallback + } + let formatter = DateFormatter() + formatter.timeStyle = .short + formatter.locale = Locale(identifier: "fr_FR") + return formatter.string(from: date) + } +} + +private enum TransportMode { + case trip + case carpool +} + +private struct ActionTile: View { + let title: String + let icon: String + var isEnabled: Bool = true + let action: () -> Void + + var body: some View { + Button(action: action) { + VStack(spacing: 8) { + Image(systemName: icon) + .font(.system(size: 18, weight: .semibold)) + Text(title) + .font(.caption.weight(.semibold)) + } + .frame(maxWidth: .infinity) + .frame(height: 64) + .foregroundStyle(isEnabled ? .primary : .secondary) + .background(Color(.tertiarySystemFill).opacity(isEnabled ? 1 : 0.6)) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + } + .disabled(!isEnabled) + .buttonStyle(ScaleButtonStyle()) + } +} + +private struct AvatarCircle: View { + let label: String + + var body: some View { + Text(label) + .font(.caption.weight(.semibold)) + .foregroundStyle(.primary) + .frame(width: 28, height: 28) + .background(Color(.tertiarySystemFill)) + .clipShape(Circle()) + } +} diff --git a/iosApp/src/Views/HomeView.swift b/iosApp/src/Views/HomeView.swift index d54f8d86..9297aabe 100644 --- a/iosApp/src/Views/HomeView.swift +++ b/iosApp/src/Views/HomeView.swift @@ -142,11 +142,11 @@ struct EventTheme { struct HomeView: View { let userId: String let repository: EventRepositoryInterface - let onEventSelected: (Event) -> Void + let onEventSelected: (Event_) -> Void let onCreateEvent: () -> Void let onProfileClick: () -> Void - @State private var events: [Event] = [] + @State private var events: [Event_] = [] @State private var isLoading = true @State private var showFilterMenu = false @State private var selectedFilter: HomeEventFilter = .upcoming @@ -189,7 +189,7 @@ struct HomeView: View { .onAppear { loadEvents() } } - private var filteredEvents: [Event] { + private var filteredEvents: [Event_] { events.filter { event in switch selectedFilter { case .upcoming: @@ -281,8 +281,8 @@ struct HomeView: View { // MARK: - Events Carousel View struct EventsCarouselView: View { - let events: [Event] - let onEventSelected: (Event) -> Void + let events: [Event_] + let onEventSelected: (Event_) -> Void let userId: String var body: some View { @@ -305,7 +305,7 @@ struct EventsCarouselView: View { // MARK: - Visual Event Card struct VisualEventCard: View { - let event: Event + let event: Event_ let isOrganizer: Bool let onTap: () -> Void diff --git a/iosApp/src/Views/InboxView.swift b/iosApp/src/Views/InboxView.swift index 408fd9ca..4c0c501f 100644 --- a/iosApp/src/Views/InboxView.swift +++ b/iosApp/src/Views/InboxView.swift @@ -81,7 +81,7 @@ struct InboxView: View { .listStyle(.plain) } } - .navigationTitle(isSelectionMode ? "\(selectedItemIds.count) selected" : "Inbox") + .navigationTitle(isSelectionMode ? "\(selectedItemIds.count) selected" : "Messages") #if os(iOS) .navigationBarTitleDisplayMode(isSelectionMode ? .inline : .large) .toolbar { @@ -161,33 +161,25 @@ struct InboxView: View { // Inbox filter with dropdown FilterTabButton( - title: "Inbox", + title: String(localized: "inbox.filter.all", defaultValue: "Tous"), isSelected: selectedFilter == .inbox && selectedEventFilter == nil, - hasDropdown: true, + hasDropdown: false, action: { selectedFilter = .inbox selectedEventFilter = nil } ) - // Focused filter with "New" badge - FilterTabButton( - title: "Focused", - isSelected: selectedFilter == .focused, - badge: "New", - action: { selectedFilter = .focused } - ) - // Unread filter FilterTabButton( - title: "Unread", + title: String(localized: "inbox.filter.unread", defaultValue: "Non lus"), isSelected: selectedFilter == .unread, action: { selectedFilter = .unread } ) // Event filter with sheet EventFilterTabButton( - title: selectedEventFilter ?? "Event", + title: selectedEventFilter ?? String(localized: "inbox.filter.groups", defaultValue: "Groupes"), isSelected: selectedFilter == .event, hasDropdown: true, action: { diff --git a/iosApp/src/Views/ParticipantManagementView.swift b/iosApp/src/Views/ParticipantManagementView.swift index c859d4d5..d1de6f4c 100644 --- a/iosApp/src/Views/ParticipantManagementView.swift +++ b/iosApp/src/Views/ParticipantManagementView.swift @@ -4,7 +4,7 @@ import Shared /// Participant management view inspired by Apple Invites /// Features: Clean list design, easy participant management, clear status indicators struct ParticipantManagementView: View { - let event: Event + let event: Event_ let repository: EventRepositoryInterface let onParticipantsUpdated: () -> Void let onBack: () -> Void diff --git a/iosApp/src/Views/PollResultsView.swift b/iosApp/src/Views/PollResultsView.swift index bd1fec01..a9f13a96 100644 --- a/iosApp/src/Views/PollResultsView.swift +++ b/iosApp/src/Views/PollResultsView.swift @@ -4,7 +4,7 @@ import Shared /// Poll results view inspired by Apple Invites /// Features: Clean results visualization, progress indicators, clear winner highlighting struct PollResultsView: View { - let event: Event + let event: Event_ let repository: EventRepositoryInterface let userId: String let onDateConfirmed: (String) -> Void @@ -12,7 +12,7 @@ struct PollResultsView: View { @State private var poll: Poll? @State private var slotScores: [PollLogic.SlotScore] = [] - @State private var bestSlot: TimeSlot? + @State private var bestSlot: TimeSlot_? @State private var isLoading = false @State private var errorMessage = "" @State private var showError = false @@ -214,7 +214,7 @@ struct PollResultsView: View { // MARK: - Best Slot Card struct BestSlotCard: View { - let slot: TimeSlot + let slot: TimeSlot_ var body: some View { VStack(spacing: 16) { @@ -277,8 +277,8 @@ struct BestSlotCard: View { // MARK: - Confirmed Date Card struct ConfirmedDateCard: View { - let event: Event - let finalSlot: TimeSlot? + let event: Event_ + let finalSlot: TimeSlot_? var body: some View { VStack(spacing: 20) { @@ -354,7 +354,7 @@ struct ConfirmedDateCard: View { // MARK: - Slot Result Card struct SlotResultCard: View { - let slot: TimeSlot + let slot: TimeSlot_ let score: PollLogic.SlotScore let isBest: Bool diff --git a/iosApp/src/Views/PollVotingView.swift b/iosApp/src/Views/PollVotingView.swift index 6de22d20..ea5954ec 100644 --- a/iosApp/src/Views/PollVotingView.swift +++ b/iosApp/src/Views/PollVotingView.swift @@ -4,13 +4,13 @@ import Shared /// Poll voting view inspired by Apple Invites /// Features: Clean design, card-based time slots, clear voting options struct PollVotingView: View { - let event: Event + let event: Event_ let repository: EventRepositoryInterface let participantId: String let onVoteSubmitted: () -> Void let onBack: () -> Void - @State private var votes: [String: PollVote] = [:] + @State private var votes: [String: Vote_] = [:] @State private var isLoading = false @State private var errorMessage = "" @State private var showError = false @@ -211,7 +211,7 @@ struct PollVotingView: View { private func checkExistingVotes() { if let poll = repository.getPoll(eventId: event.id) { - if let participantVoteMap = poll.votes[participantId] as? [String: PollVote] { + if let participantVoteMap = poll.votes[participantId] { hasVoted = !participantVoteMap.isEmpty if hasVoted { @@ -222,24 +222,15 @@ struct PollVotingView: View { } private func submitVotes() async { - guard votes.count == event.proposedSlots.count else { return } + guard !event.proposedSlots.isEmpty, votes.count == event.proposedSlots.count else { return } isLoading = true var successCount = 0 var lastError: Error? - for (slotId, pollVote) in votes { + for (slotId, sharedVote) in votes { do { - // Convert PollVote to Shared.Vote - let sharedVote: Shared.Vote = { - switch pollVote { - case .yes: return .yes - case .maybe: return .maybe - case .no: return .no - } - }() - let result = try await repository.addVote( eventId: event.id, participantId: participantId, @@ -314,9 +305,9 @@ struct VoteGuideRow: View { // MARK: - Time Slot Vote Card struct TimeSlotVoteCard: View { - let timeSlot: TimeSlot - let selectedVote: PollVote? - let onVoteSelected: (PollVote) -> Void + let timeSlot: TimeSlot_ + let selectedVote: Vote_? + let onVoteSelected: (Vote_) -> Void var body: some View { VStack(spacing: 16) { @@ -393,7 +384,7 @@ struct TimeSlotVoteCard: View { // MARK: - Vote Button struct VoteButton: View { - let vote: PollVote + let vote: Vote_ let icon: String let label: String let color: Color