Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ kotlin {
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.accompanist.swiperefresh)
}
commonMain.dependencies {
implementation(libs.compose.runtime)
Expand Down
423 changes: 349 additions & 74 deletions composeApp/src/androidMain/kotlin/friends/mobile/main/MainView.kt

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ junit = "4.13.2"
kotlin = "2.3.0"
material3 = "1.10.0-alpha05"
androidx-navigation = "2.8.5"
accompanist = "0.34.0"

[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
Expand Down Expand Up @@ -58,6 +59,7 @@ compose-components-resources = { module = "org.jetbrains.compose.components:comp
compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
compose-material-icons-extended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "composeIcons" }
accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanist" }

[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
Expand Down
10 changes: 10 additions & 0 deletions iosApp/Utils/EventDetail+Identifiable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// EventDetail+Identifiable.swift
// iosApp
//
// Created by Данил Забинский on 18.05.2026.
//

import Shared

extension EventDetail: @retroactive Identifiable {}
41 changes: 41 additions & 0 deletions iosApp/iosApp/Helper/ToastModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// ToastModifier.swift
// iosApp
//
// Created by Данил Забинский on 18.05.2026.
//

import SwiftUI

struct ToastModifier: ViewModifier {
@State var isPresented: Bool
let message: String

func body(content: Content) -> some View {
ZStack {
content

if isPresented {
VStack {
Text(message)
.font(.subheadline)
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 10)
.background(Color.black.opacity(0.8))
.cornerRadius(8)
.padding()

Spacer()
}
.transition(.opacity)
}
}
}
}

extension View {
func toast(isPresented: Binding<Bool>, message: String) -> some View {
modifier(ToastModifier(isPresented: isPresented.wrappedValue, message: message))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//
// PendingEventDetailSheet.swift
// iosApp
//
// Created by Данил Забинский on 18.05.2026.
//

import SwiftUI
import Shared

struct PendingEventDetailSheet: View {

let eventDetail: EventDetail
let isLoading: Bool
let error: String?
let onAccept: () -> Void
let onDecline: () -> Void
let onDismiss: () -> Void

@Environment(\.dismiss) var dismiss

private func statusColor(_ status: String) -> Color {
switch status.lowercased() {
case "accepted":
return .green
case "declined":
return .red
case "pending":
return .orange
default:
return .gray
}
}

var body: some View {
NavigationStack {
if let error = error {
VStack {
Text(error)
.foregroundColor(.red)
.multilineTextAlignment(.center)
.padding()
}
} else if isLoading {
ProgressView()
} else {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text(eventDetail.title)
.font(.title2)
.fontWeight(.bold)

if let description = eventDetail.description_ {
Text(description)
.font(.body)
.foregroundColor(.secondary)
}
}

Divider()

VStack(alignment: .leading, spacing: 12) {
Label(eventDetail.date, systemImage: "calendar")
.font(.subheadline)

if let time = eventDetail.time {
Label(time, systemImage: "clock")
.font(.subheadline)
}

if let location = eventDetail.location {
Label(location, systemImage: "location.fill")
.font(.subheadline)
}

HStack {
Text("Status:")
.font(.subheadline)
Spacer()
Text(eventDetail.status)
.font(.subheadline)
.foregroundColor(statusColor(eventDetail.status))
.fontWeight(.semibold)
}
}

Divider()

VStack(alignment: .leading, spacing: 12) {
Text("Participants")
.font(.headline)

VStack(spacing: 12) {
ForEach(eventDetail.participants, id: \.userId) { participant in
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text(participant.username)
.font(.body)
.foregroundStyle(.primary)
Text(participant.role)
.font(.caption)
.foregroundStyle(.gray)
}

Spacer()

Text(participant.responseStatus)
.font(.subheadline)
.foregroundColor(statusColor(participant.responseStatus))
}
.padding(.vertical, 4)
}
}
}

Spacer()
.frame(height: 16)

VStack(spacing: 12) {
Button(action: onAccept) {
Text("Accept Invitation")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(Color.green)
.foregroundColor(.white)
.cornerRadius(8)
}
.disabled(isLoading)

Button(action: onDecline) {
Text("Decline Invitation")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(8)
}
.disabled(isLoading)
}
}
.padding()
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Close") {
dismiss()
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//
// PendingEventListView.swift
// iosApp
//
// Created by Данил Забинский on 18.05.2026.
//

import SwiftUI
import Shared

struct PendingEventListView: View {

@State private var reducer = PendingEventReducer()
@Environment(Router.self) private var router

var body: some View {
VStack {
if reducer.isLoading {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let errorMessage = reducer.errorMessage {
VStack {
Text(errorMessage)
.foregroundColor(.red)
.multilineTextAlignment(.center)
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
List(reducer.pendingEvents, id: \.id) { event in
VStack(alignment: .leading, spacing: 8) {
Text(event.title)
.font(.headline)

HStack(spacing: 12) {
Image(systemName: "calendar")
.foregroundColor(.gray)
Text(event.date)
.font(.subheadline)
.foregroundColor(.gray)
}

if let time = event.time {
HStack(spacing: 12) {
Image(systemName: "clock")
.foregroundColor(.gray)
Text(time)
.font(.subheadline)
.foregroundColor(.gray)
}
}

HStack(spacing: 12) {
Image(systemName: "person.2")
.foregroundColor(.gray)
Text("\(event.participants.count) participants")
.font(.subheadline)
.foregroundColor(.gray)
}
}
.padding(.vertical, 4)
.onTapGesture {
reducer.fetchEventDetail(eventId: event.id)
}
}
.refreshable {
reducer.refresh()
}
}
}
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button {
router.pop()
} label: {
Image(systemName: "chevron.left")
.padding()
}
}
}
.navigationTitle("Pending Invitations")
.sheet(item: Binding(
get: { reducer.selectedEventDetail },
set: { _ in reducer.closeEventDetail() }
)) { eventDetail in
PendingEventDetailSheet(
eventDetail: eventDetail,
isLoading: reducer.isLoadingDetail,
error: reducer.detailError,
onAccept: { reducer.acceptEvent(eventId: eventDetail.id) },
onDecline: { reducer.declineEvent(eventId: eventDetail.id) },
onDismiss: { reducer.closeEventDetail() }
)
}
.toast(isPresented: $reducer.showToast, message: reducer.toastMessage ?? "")
}
}
Loading
Loading