From 8625965477a8792a542e1d9aaa22019365191352 Mon Sep 17 00:00:00 2001 From: kennanLu Date: Sun, 16 Nov 2025 20:04:41 -0500 Subject: [PATCH 01/31] chore: make base file structure --- .../kotlin/com/example/demo/feature/rides/FindARide.kt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindARide.kt diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindARide.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindARide.kt new file mode 100644 index 0000000..bcf0cc1 --- /dev/null +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindARide.kt @@ -0,0 +1,4 @@ +package com.example.demo.feature.rides + +class FindARide { +} \ No newline at end of file From 431f30131aeb8a9c7178c6142e37d4374e7474e6 Mon Sep 17 00:00:00 2001 From: kennanLu Date: Sun, 16 Nov 2025 21:46:02 -0500 Subject: [PATCH 02/31] feature(rides): initialized FindRideScreen.kt and created header section --- .../drawable/ic_back_arrow.xml | 12 ++++ .../example/demo/feature/rides/FindARide.kt | 4 -- .../demo/feature/rides/FindRideScreen.kt | 68 +++++++++++++++++++ 3 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 app/composeApp/src/commonMain/composeResources/drawable/ic_back_arrow.xml delete mode 100644 app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindARide.kt create mode 100644 app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt diff --git a/app/composeApp/src/commonMain/composeResources/drawable/ic_back_arrow.xml b/app/composeApp/src/commonMain/composeResources/drawable/ic_back_arrow.xml new file mode 100644 index 0000000..125cf9c --- /dev/null +++ b/app/composeApp/src/commonMain/composeResources/drawable/ic_back_arrow.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindARide.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindARide.kt deleted file mode 100644 index bcf0cc1..0000000 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindARide.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.demo.feature.rides - -class FindARide { -} \ No newline at end of file diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt new file mode 100644 index 0000000..4edcb4e --- /dev/null +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt @@ -0,0 +1,68 @@ +package com.example.demo.ui.theme + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import app.composeapp.generated.resources.Res +import app.composeapp.generated.resources.ic_back_arrow + +import org.jetbrains.compose.resources.painterResource + +@Composable +fun FindRideScreen( + onBackClick: () -> Unit, +) { + val bgColor = Color(0xFFF3F4F6) + + Box( + modifier = Modifier + .fillMaxSize() + .background(bgColor) + ) { + // 1. Header Section + Column( + modifier = Modifier + .fillMaxWidth() + .height(200.dp) // Covers top portion + .background(DrexelBlue) + .padding(24.dp) + ) { + IconButton(onClick = onBackClick) { + Icon( + painter = painterResource(Res.drawable.ic_back_arrow), + contentDescription = "Back", + tint = Color.White, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Find a Ride", + style = MaterialTheme.typography.titleLarge, + color = Color.White, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Enter your trip details", + style = MaterialTheme.typography.titleLarge, + color = HintGrey, + ) + } + } +} \ No newline at end of file From 04dd8ff9dc136f50bb0679bcd420cc08c560b289 Mon Sep 17 00:00:00 2001 From: kennanLu Date: Sun, 16 Nov 2025 21:55:25 -0500 Subject: [PATCH 03/31] docs: updated dev journal --- docs/dev_journal/Kennan.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/dev_journal/Kennan.md b/docs/dev_journal/Kennan.md index a772d0d..2a39d46 100644 --- a/docs/dev_journal/Kennan.md +++ b/docs/dev_journal/Kennan.md @@ -1,3 +1,30 @@ +# 11/16/2025 + +## Development Strategy + +As we team we focused and came up with the strategy with 3 Phases. + +We are following Three Phase Strategy: + +UI Development -> UI Connection Development -> Frontend to Backend Database Connection + +And [ChatGPT](https://chatgpt.com/share/691a8a83-69c0-800e-be1c-aa304a8a901f) verified this is the correct process that modern companies use. + +Following paging structure with react where we have folders for our features. + +## Importing Icons + +You should use your own SVG/XML files if you want to import icons onto the UI for the screens. + + 1. Get your icons - Download the icons you want as .svg or .xml (Android Vector) files. (Sites like Heroicons or Phosphor Icons are good sources). + + 2. Place them in the resources folder Go to composeApp/src/commonMain/composeResources/drawable. Paste your files there (e.g., ic_calendar.xml, ic_location.xml). + + 3. Update the code to use painterResource + +EXAMPLE CALL using painterResource: +icon = painterResource(Res.drawable.ic_calendar) + # 11/2/2025 - Initialize the SQLite Database From a8bb67ebe862b4cc429cdadc21240268a27bfe87 Mon Sep 17 00:00:00 2001 From: kennanLu Date: Thu, 20 Nov 2025 23:48:50 -0500 Subject: [PATCH 04/31] feature(rides): initialized floating white card for the UI --- .../demo/feature/rides/FindRideScreen.kt | 60 ++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt index 4edcb4e..2e71a1a 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt @@ -1,6 +1,8 @@ package com.example.demo.ui.theme +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -8,13 +10,23 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.composeapp.generated.resources.Res import app.composeapp.generated.resources.ic_back_arrow @@ -22,10 +34,9 @@ import app.composeapp.generated.resources.ic_back_arrow import org.jetbrains.compose.resources.painterResource @Composable -fun FindRideScreen( - onBackClick: () -> Unit, -) { +fun FindRideScreen() { val bgColor = Color(0xFFF3F4F6) + val scrollState = rememberScrollState() Box( modifier = Modifier @@ -40,11 +51,11 @@ fun FindRideScreen( .background(DrexelBlue) .padding(24.dp) ) { - IconButton(onClick = onBackClick) { + IconButton(onClick = {}) { Icon( painter = painterResource(Res.drawable.ic_back_arrow), contentDescription = "Back", - tint = Color.White, + tint = Color.White ) } @@ -53,7 +64,7 @@ fun FindRideScreen( Text( text = "Find a Ride", style = MaterialTheme.typography.titleLarge, - color = Color.White, + color = Color.White ) Spacer(modifier = Modifier.height(8.dp)) @@ -61,8 +72,43 @@ fun FindRideScreen( Text( text = "Enter your trip details", style = MaterialTheme.typography.titleLarge, - color = HintGrey, + color = HintGrey ) } + + // 2. The Floating Card + Card( + modifier = Modifier + .fillMaxWidth() + .padding(top = 175.dp, start = 16.dp, end = 16.dp, bottom = 16.dp) + .verticalScroll(scrollState), // Make form scrollable on small screens + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + + } + } + } +} + +@Composable +fun RideInput( + label: String, + icon: androidx.compose.ui.graphics.vector.ImageVector, + value: String, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + Text( + text = label, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(bottom = 4.dp) + ) } } \ No newline at end of file From 37719b50ea92d9a212711af091c3efb1e6c0b79d Mon Sep 17 00:00:00 2001 From: kennanLu Date: Fri, 21 Nov 2025 01:11:35 -0500 Subject: [PATCH 05/31] docs: updated dev journal --- docs/dev_journal/Kennan.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/dev_journal/Kennan.md b/docs/dev_journal/Kennan.md index 2a39d46..0184c89 100644 --- a/docs/dev_journal/Kennan.md +++ b/docs/dev_journal/Kennan.md @@ -1,3 +1,9 @@ +# 11/20/2025 + +Experienced a bunch of issues with trying to build and run the app. + +Solution: Deleted the .gradle folder in the app subdirectory and forced it to be rebuilt. + # 11/16/2025 ## Development Strategy From 9589b7b5486c0b157bd183eeb84d827a655c755e Mon Sep 17 00:00:00 2001 From: kennanLu Date: Fri, 21 Nov 2025 01:17:24 -0500 Subject: [PATCH 06/31] feature(rides): intialized the rest of the input fields for FindRideScreen.kt --- .../kotlin/com/example/demo/MainActivity.kt | 3 +- .../demo/feature/rides/FindRideScreen.kt | 118 ++++++++++++++---- 2 files changed, 96 insertions(+), 25 deletions(-) diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt index aaffeff..22f4d1b 100644 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt @@ -6,6 +6,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +import com.example.demo.ui.theme.FindRideScreen class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -13,7 +14,7 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { - App() + FindRideScreen() } } } diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt index 2e71a1a..13af987 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt @@ -1,36 +1,20 @@ package com.example.demo.ui.theme -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import app.composeapp.generated.resources.Res import app.composeapp.generated.resources.ic_back_arrow - import org.jetbrains.compose.resources.painterResource @Composable @@ -47,7 +31,7 @@ fun FindRideScreen() { Column( modifier = Modifier .fillMaxWidth() - .height(200.dp) // Covers top portion + .height(200.dp) .background(DrexelBlue) .padding(24.dp) ) { @@ -90,7 +74,62 @@ fun FindRideScreen() { modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { + // Locations + RideInput(label = "Pickup Location", icon = painterResource(Res.drawable.ic_back_arrow), value = "30th Street Station") + RideInput(label = "Drop-off Location", icon = painterResource(Res.drawable.ic_back_arrow), value = "Cira Green") + + // Date & Time + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + RideInput( + label = "Date", + icon = painterResource(Res.drawable.ic_back_arrow), + value = "mm/dd/yyyy", + modifier = Modifier.weight(1f) + ) + RideInput( + label = "Time", + icon = painterResource(Res.drawable.ic_back_arrow), + value = "--:-- --", + modifier = Modifier.weight(1f) + ) + } + + // Seats & Price + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + RideInput( + label = "Seats Needed", + icon = painterResource(Res.drawable.ic_back_arrow), + value = "1 Seat", + modifier = Modifier.weight(1f) + ) + RideInput( + label = "Max Price", + icon = painterResource(Res.drawable.ic_back_arrow), + value = "20", + modifier = Modifier.weight(1f) + ) + } + Spacer(modifier = Modifier.height(16.dp)) + + // Search Button + Button( + onClick = {}, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + colors = ButtonDefaults.buttonColors( + containerColor = DrexelGold, + contentColor = DrexelBlue + ), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Search for Rides", + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } } } } @@ -99,16 +138,47 @@ fun FindRideScreen() { @Composable fun RideInput( label: String, - icon: androidx.compose.ui.graphics.vector.ImageVector, + icon: Painter, value: String, modifier: Modifier = Modifier ) { + var textState by remember { mutableStateOf(value) } + val fieldColor = Color(0xFFF3F4F6) + Column(modifier = modifier) { Text( text = label, - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleMedium, + color = DrexelBlue, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(bottom = 4.dp) ) + + OutlinedTextField( + value = textState, + onValueChange = { textState = it }, + + modifier = Modifier.fillMaxWidth(), + leadingIcon = { + Icon( + painter = icon, + contentDescription = null, + tint = HintGrey, + modifier = Modifier.size(20.dp) + ) + }, + + shape = RoundedCornerShape(8.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = fieldColor, + unfocusedContainerColor = fieldColor, + disabledContainerColor = fieldColor, + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + focusedTextColor = DrexelBlue, + unfocusedTextColor = DrexelBlue + ), + singleLine = true, + ) } } \ No newline at end of file From 62457fba5e3d780a3ca8e8b5da0ac37291503cfc Mon Sep 17 00:00:00 2001 From: kennanLu Date: Fri, 21 Nov 2025 01:20:48 -0500 Subject: [PATCH 07/31] feature(rides): initialized OfferRideScreen.kt for tracking ride offer requests --- .../kotlin/com/example/demo/feature/rides/OfferRideScreen.kt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt new file mode 100644 index 0000000..d777768 --- /dev/null +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt @@ -0,0 +1,2 @@ +package com.example.demo.ui.theme + From eb37b773c90d007ab51ae33a8fbdb77a8716b63f Mon Sep 17 00:00:00 2001 From: kennanLu Date: Fri, 21 Nov 2025 01:44:20 -0500 Subject: [PATCH 08/31] feature(rides): initialized input fields for OfferRideScreen.kt --- .../kotlin/com/example/demo/MainActivity.kt | 5 +- .../demo/feature/rides/FindRideScreen.kt | 2 +- .../demo/feature/rides/OfferRideScreen.kt | 139 ++++++++++++++++++ 3 files changed, 143 insertions(+), 3 deletions(-) diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt index 22f4d1b..c654fcf 100644 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt @@ -7,6 +7,7 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import com.example.demo.ui.theme.FindRideScreen +import com.example.demo.ui.theme.OfferRideScreen class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -14,7 +15,7 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { - FindRideScreen() + OfferRideScreen() } } } @@ -22,5 +23,5 @@ class MainActivity : ComponentActivity() { @Preview @Composable fun AppAndroidPreview() { - App() + OfferRideScreen() } \ No newline at end of file diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt index 13af987..3583a66 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt @@ -110,7 +110,7 @@ fun FindRideScreen() { ) } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(4.dp)) // Search Button Button( diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt index d777768..0049677 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt @@ -1,2 +1,141 @@ package com.example.demo.ui.theme +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.composeapp.generated.resources.Res +import app.composeapp.generated.resources.ic_back_arrow +import org.jetbrains.compose.resources.painterResource + +@Composable +fun OfferRideScreen() { + val bgColor = Color(0xFFF3F4F6) + val scrollState = rememberScrollState() + + Box( + modifier = Modifier + .fillMaxSize() + .background(bgColor) + ) { + // 1. Header Section + Column( + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .background(DrexelBlue) + .padding(24.dp) + ) { + IconButton(onClick = {}) { + Icon( + painter = painterResource(Res.drawable.ic_back_arrow), + contentDescription = "Back", + tint = Color.White + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Offer a Ride", + style = MaterialTheme.typography.titleLarge, + color = Color.White + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Share your journey details", + style = MaterialTheme.typography.titleLarge, + color = HintGrey + ) + } + + // 2. The Floating Card + Card( + modifier = Modifier + .fillMaxWidth() + .padding(top = 175.dp, start = 16.dp, end = 16.dp, bottom = 16.dp) + .verticalScroll(scrollState), // Make form scrollable on small screens + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Locations + RideInput(label = "Origin Location", icon = painterResource(Res.drawable.ic_back_arrow), value = "University Crossings") + RideInput(label = "Destination", icon = painterResource(Res.drawable.ic_back_arrow), value = "Korman Center") + + // Departure Time + RideInput( + label = "Departure Time", + icon = painterResource(Res.drawable.ic_back_arrow), + value = "mm/dd/yyyy --:-- --", + ) + + // Vehicle Selection + RideInput( + label = "Choose Vehicle", + icon = painterResource(Res.drawable.ic_back_arrow), + value = "Tesla Model Y (Blue)", + ) + + // Available Seats + RideInput( + label = "Available Seats", + icon = painterResource(Res.drawable.ic_back_arrow), + value = "2 Seats", + ) + + // Base Price & Price Per Mile + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + RideInput( + label = "Base Price", + icon = painterResource(Res.drawable.ic_back_arrow), + value = "5.00", + modifier = Modifier.weight(1f) + ) + RideInput( + label = "Per Mile", + icon = painterResource(Res.drawable.ic_back_arrow), + value = "0.50", + modifier = Modifier.weight(1f) + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + // Search Button + Button( + onClick = {}, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + colors = ButtonDefaults.buttonColors( + containerColor = DrexelGold, + contentColor = DrexelBlue + ), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Publish Offer", + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } + } + } +} \ No newline at end of file From c0ee6fb14a3f9c4be9ddef31634a03a23df7f535 Mon Sep 17 00:00:00 2001 From: kennanLu Date: Fri, 21 Nov 2025 01:52:12 -0500 Subject: [PATCH 09/31] docs: updated dev journal entry from 11/16/2025 to provide additonal steps for image insertion --- docs/dev_journal/Kennan.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/dev_journal/Kennan.md b/docs/dev_journal/Kennan.md index 0184c89..01e91bf 100644 --- a/docs/dev_journal/Kennan.md +++ b/docs/dev_journal/Kennan.md @@ -31,6 +31,22 @@ You should use your own SVG/XML files if you want to import icons onto the UI fo EXAMPLE CALL using painterResource: icon = painterResource(Res.drawable.ic_calendar) +#### 11/21/2025 Update: ANDROID DOES NOT SUPPORT SVG FILES + +Here's how to convert them to XML before use: + + 1. In the Project view (left panel), right-click on your commonMain/composeResources/drawable folder. + + 2. Select New > Vector Asset. + + 3. In the "Asset Type" section, choose Local file (SVG, PSD). + + 4. Click the folder icon next to Path and select your original SVG file. + + 5. Click Next and then Finish. + +This will create a new .xml file in that folder. Delete the old .svg file so there is no confusion. + # 11/2/2025 - Initialize the SQLite Database From e758c13939a653d8bb42871a0762fddb3cb7476c Mon Sep 17 00:00:00 2001 From: kennanLu Date: Fri, 21 Nov 2025 02:29:31 -0500 Subject: [PATCH 10/31] feature(rides): updated placeholder icons with favicons --- .../composeResources/drawable/ic_calendar.xml | 13 +++++++++++++ .../composeResources/drawable/ic_clock.xml | 13 +++++++++++++ .../drawable/ic_dollar_sign.xml | 13 +++++++++++++ .../drawable/ic_location_ping.xml | 9 +++++++++ .../drawable/ic_two_people.xml | 10 ++++++++++ .../composeResources/drawable/ic_vehicle.xml | 15 +++++++++++++++ .../demo/feature/rides/FindRideScreen.kt | 19 ++++++++++++------- .../demo/feature/rides/OfferRideScreen.kt | 19 ++++++++++++------- 8 files changed, 97 insertions(+), 14 deletions(-) create mode 100644 app/composeApp/src/commonMain/composeResources/drawable/ic_calendar.xml create mode 100644 app/composeApp/src/commonMain/composeResources/drawable/ic_clock.xml create mode 100644 app/composeApp/src/commonMain/composeResources/drawable/ic_dollar_sign.xml create mode 100644 app/composeApp/src/commonMain/composeResources/drawable/ic_location_ping.xml create mode 100644 app/composeApp/src/commonMain/composeResources/drawable/ic_two_people.xml create mode 100644 app/composeApp/src/commonMain/composeResources/drawable/ic_vehicle.xml diff --git a/app/composeApp/src/commonMain/composeResources/drawable/ic_calendar.xml b/app/composeApp/src/commonMain/composeResources/drawable/ic_calendar.xml new file mode 100644 index 0000000..0cd1ed0 --- /dev/null +++ b/app/composeApp/src/commonMain/composeResources/drawable/ic_calendar.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/composeApp/src/commonMain/composeResources/drawable/ic_clock.xml b/app/composeApp/src/commonMain/composeResources/drawable/ic_clock.xml new file mode 100644 index 0000000..df68293 --- /dev/null +++ b/app/composeApp/src/commonMain/composeResources/drawable/ic_clock.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/composeApp/src/commonMain/composeResources/drawable/ic_dollar_sign.xml b/app/composeApp/src/commonMain/composeResources/drawable/ic_dollar_sign.xml new file mode 100644 index 0000000..21e87fc --- /dev/null +++ b/app/composeApp/src/commonMain/composeResources/drawable/ic_dollar_sign.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/composeApp/src/commonMain/composeResources/drawable/ic_location_ping.xml b/app/composeApp/src/commonMain/composeResources/drawable/ic_location_ping.xml new file mode 100644 index 0000000..fae4e07 --- /dev/null +++ b/app/composeApp/src/commonMain/composeResources/drawable/ic_location_ping.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/composeApp/src/commonMain/composeResources/drawable/ic_two_people.xml b/app/composeApp/src/commonMain/composeResources/drawable/ic_two_people.xml new file mode 100644 index 0000000..05f423f --- /dev/null +++ b/app/composeApp/src/commonMain/composeResources/drawable/ic_two_people.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/composeApp/src/commonMain/composeResources/drawable/ic_vehicle.xml b/app/composeApp/src/commonMain/composeResources/drawable/ic_vehicle.xml new file mode 100644 index 0000000..b50b2b1 --- /dev/null +++ b/app/composeApp/src/commonMain/composeResources/drawable/ic_vehicle.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt index 3583a66..851419c 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt @@ -15,6 +15,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.composeapp.generated.resources.Res import app.composeapp.generated.resources.ic_back_arrow +import app.composeapp.generated.resources.ic_location_ping +import app.composeapp.generated.resources.ic_calendar +import app.composeapp.generated.resources.ic_clock +import app.composeapp.generated.resources.ic_two_people +import app.composeapp.generated.resources.ic_dollar_sign import org.jetbrains.compose.resources.painterResource @Composable @@ -75,20 +80,20 @@ fun FindRideScreen() { verticalArrangement = Arrangement.spacedBy(16.dp) ) { // Locations - RideInput(label = "Pickup Location", icon = painterResource(Res.drawable.ic_back_arrow), value = "30th Street Station") - RideInput(label = "Drop-off Location", icon = painterResource(Res.drawable.ic_back_arrow), value = "Cira Green") + RideInput(label = "Pickup Location", icon = painterResource(Res.drawable.ic_location_ping), value = "30th Street Station") + RideInput(label = "Drop-off Location", icon = painterResource(Res.drawable.ic_location_ping), value = "Cira Green") // Date & Time Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { RideInput( label = "Date", - icon = painterResource(Res.drawable.ic_back_arrow), + icon = painterResource(Res.drawable.ic_calendar), value = "mm/dd/yyyy", modifier = Modifier.weight(1f) ) RideInput( label = "Time", - icon = painterResource(Res.drawable.ic_back_arrow), + icon = painterResource(Res.drawable.ic_clock), value = "--:-- --", modifier = Modifier.weight(1f) ) @@ -98,13 +103,13 @@ fun FindRideScreen() { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { RideInput( label = "Seats Needed", - icon = painterResource(Res.drawable.ic_back_arrow), + icon = painterResource(Res.drawable.ic_two_people), value = "1 Seat", modifier = Modifier.weight(1f) ) RideInput( label = "Max Price", - icon = painterResource(Res.drawable.ic_back_arrow), + icon = painterResource(Res.drawable.ic_dollar_sign), value = "20", modifier = Modifier.weight(1f) ) @@ -164,7 +169,7 @@ fun RideInput( painter = icon, contentDescription = null, tint = HintGrey, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(28.dp) ) }, diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt index 0049677..479b2c8 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt @@ -15,6 +15,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.composeapp.generated.resources.Res import app.composeapp.generated.resources.ic_back_arrow +import app.composeapp.generated.resources.ic_location_ping +import app.composeapp.generated.resources.ic_clock +import app.composeapp.generated.resources.ic_two_people +import app.composeapp.generated.resources.ic_dollar_sign +import app.composeapp.generated.resources.ic_vehicle import org.jetbrains.compose.resources.painterResource @Composable @@ -75,27 +80,27 @@ fun OfferRideScreen() { verticalArrangement = Arrangement.spacedBy(16.dp) ) { // Locations - RideInput(label = "Origin Location", icon = painterResource(Res.drawable.ic_back_arrow), value = "University Crossings") - RideInput(label = "Destination", icon = painterResource(Res.drawable.ic_back_arrow), value = "Korman Center") + RideInput(label = "Origin Location", icon = painterResource(Res.drawable.ic_location_ping), value = "University Crossings") + RideInput(label = "Destination", icon = painterResource(Res.drawable.ic_location_ping), value = "Korman Center") // Departure Time RideInput( label = "Departure Time", - icon = painterResource(Res.drawable.ic_back_arrow), + icon = painterResource(Res.drawable.ic_clock), value = "mm/dd/yyyy --:-- --", ) // Vehicle Selection RideInput( label = "Choose Vehicle", - icon = painterResource(Res.drawable.ic_back_arrow), + icon = painterResource(Res.drawable.ic_vehicle), value = "Tesla Model Y (Blue)", ) // Available Seats RideInput( label = "Available Seats", - icon = painterResource(Res.drawable.ic_back_arrow), + icon = painterResource(Res.drawable.ic_two_people), value = "2 Seats", ) @@ -103,13 +108,13 @@ fun OfferRideScreen() { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { RideInput( label = "Base Price", - icon = painterResource(Res.drawable.ic_back_arrow), + icon = painterResource(Res.drawable.ic_dollar_sign), value = "5.00", modifier = Modifier.weight(1f) ) RideInput( label = "Per Mile", - icon = painterResource(Res.drawable.ic_back_arrow), + icon = painterResource(Res.drawable.ic_dollar_sign), value = "0.50", modifier = Modifier.weight(1f) ) From 0a8d868f0e2fb2f1f7b4bff007103ef5b5d20b21 Mon Sep 17 00:00:00 2001 From: kelvyloo Date: Wed, 26 Nov 2025 15:54:49 -0500 Subject: [PATCH 11/31] feature(rides): refactored function for input fields into its own file FieldInputs.kt, initialized fields to display placeholder text --- .../example/demo/feature/rides/FieldInputs.kt | 79 +++++++++++++++++++ .../demo/feature/rides/FindRideScreen.kt | 62 ++------------- .../demo/feature/rides/OfferRideScreen.kt | 16 ++-- 3 files changed, 94 insertions(+), 63 deletions(-) create mode 100644 app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FieldInputs.kt diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FieldInputs.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FieldInputs.kt new file mode 100644 index 0000000..1ed2d3c --- /dev/null +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FieldInputs.kt @@ -0,0 +1,79 @@ +package com.example.demo.feature.rides + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.example.demo.ui.theme.DrexelBlue +import com.example.demo.ui.theme.HintGrey + +@Composable +fun RideInput( + label: String, + icon: Painter, + placeholder: String, + modifier: Modifier = Modifier +) { + var textState by remember { mutableStateOf("") } + val fieldColor = Color(0xFFF3F4F6) + + Column(modifier = modifier) { + Text( + text = label, + style = MaterialTheme.typography.titleMedium, + color = DrexelBlue, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(bottom = 4.dp) + ) + + OutlinedTextField( + value = textState, + onValueChange = { textState = it }, + modifier = Modifier.fillMaxWidth(), + + placeholder = { + Text( + text = placeholder, + color = HintGrey + ) + }, + + leadingIcon = { + Icon( + painter = icon, + contentDescription = null, + tint = HintGrey, + modifier = Modifier.size(28.dp) + ) + }, + + shape = RoundedCornerShape(8.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = fieldColor, + unfocusedContainerColor = fieldColor, + disabledContainerColor = fieldColor, + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + focusedTextColor = DrexelBlue, + unfocusedTextColor = DrexelBlue + ), + singleLine = true, + ) + } +} \ No newline at end of file diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt index 851419c..95735af 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -21,6 +20,7 @@ import app.composeapp.generated.resources.ic_clock import app.composeapp.generated.resources.ic_two_people import app.composeapp.generated.resources.ic_dollar_sign import org.jetbrains.compose.resources.painterResource +import com.example.demo.feature.rides.RideInput @Composable fun FindRideScreen() { @@ -80,21 +80,21 @@ fun FindRideScreen() { verticalArrangement = Arrangement.spacedBy(16.dp) ) { // Locations - RideInput(label = "Pickup Location", icon = painterResource(Res.drawable.ic_location_ping), value = "30th Street Station") - RideInput(label = "Drop-off Location", icon = painterResource(Res.drawable.ic_location_ping), value = "Cira Green") + RideInput(label = "Pickup Location", icon = painterResource(Res.drawable.ic_location_ping), placeholder = "30th Street Station") + RideInput(label = "Drop-off Location", icon = painterResource(Res.drawable.ic_location_ping), placeholder = "Cira Green") // Date & Time Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { RideInput( label = "Date", icon = painterResource(Res.drawable.ic_calendar), - value = "mm/dd/yyyy", + placeholder = "mm/dd/yyyy", modifier = Modifier.weight(1f) ) RideInput( label = "Time", icon = painterResource(Res.drawable.ic_clock), - value = "--:-- --", + placeholder = "--:-- --", modifier = Modifier.weight(1f) ) } @@ -104,13 +104,13 @@ fun FindRideScreen() { RideInput( label = "Seats Needed", icon = painterResource(Res.drawable.ic_two_people), - value = "1 Seat", + placeholder = "2", modifier = Modifier.weight(1f) ) RideInput( label = "Max Price", icon = painterResource(Res.drawable.ic_dollar_sign), - value = "20", + placeholder = "20", modifier = Modifier.weight(1f) ) } @@ -138,52 +138,4 @@ fun FindRideScreen() { } } } -} - -@Composable -fun RideInput( - label: String, - icon: Painter, - value: String, - modifier: Modifier = Modifier -) { - var textState by remember { mutableStateOf(value) } - val fieldColor = Color(0xFFF3F4F6) - - Column(modifier = modifier) { - Text( - text = label, - style = MaterialTheme.typography.titleMedium, - color = DrexelBlue, - fontWeight = FontWeight.SemiBold, - modifier = Modifier.padding(bottom = 4.dp) - ) - - OutlinedTextField( - value = textState, - onValueChange = { textState = it }, - - modifier = Modifier.fillMaxWidth(), - leadingIcon = { - Icon( - painter = icon, - contentDescription = null, - tint = HintGrey, - modifier = Modifier.size(28.dp) - ) - }, - - shape = RoundedCornerShape(8.dp), - colors = OutlinedTextFieldDefaults.colors( - focusedContainerColor = fieldColor, - unfocusedContainerColor = fieldColor, - disabledContainerColor = fieldColor, - focusedBorderColor = Color.Transparent, - unfocusedBorderColor = Color.Transparent, - focusedTextColor = DrexelBlue, - unfocusedTextColor = DrexelBlue - ), - singleLine = true, - ) - } } \ No newline at end of file diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt index 479b2c8..6f50200 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -21,6 +20,7 @@ import app.composeapp.generated.resources.ic_two_people import app.composeapp.generated.resources.ic_dollar_sign import app.composeapp.generated.resources.ic_vehicle import org.jetbrains.compose.resources.painterResource +import com.example.demo.feature.rides.RideInput @Composable fun OfferRideScreen() { @@ -80,28 +80,28 @@ fun OfferRideScreen() { verticalArrangement = Arrangement.spacedBy(16.dp) ) { // Locations - RideInput(label = "Origin Location", icon = painterResource(Res.drawable.ic_location_ping), value = "University Crossings") - RideInput(label = "Destination", icon = painterResource(Res.drawable.ic_location_ping), value = "Korman Center") + RideInput(label = "Origin Location", icon = painterResource(Res.drawable.ic_location_ping), placeholder = "University Crossings") + RideInput(label = "Destination", icon = painterResource(Res.drawable.ic_location_ping), placeholder = "Korman Center") // Departure Time RideInput( label = "Departure Time", icon = painterResource(Res.drawable.ic_clock), - value = "mm/dd/yyyy --:-- --", + placeholder = "mm/dd/yyyy --:-- --", ) // Vehicle Selection RideInput( label = "Choose Vehicle", icon = painterResource(Res.drawable.ic_vehicle), - value = "Tesla Model Y (Blue)", + placeholder = "Tesla Model Y (Blue)", ) // Available Seats RideInput( label = "Available Seats", icon = painterResource(Res.drawable.ic_two_people), - value = "2 Seats", + placeholder = "2", ) // Base Price & Price Per Mile @@ -109,13 +109,13 @@ fun OfferRideScreen() { RideInput( label = "Base Price", icon = painterResource(Res.drawable.ic_dollar_sign), - value = "5.00", + placeholder = "5.00", modifier = Modifier.weight(1f) ) RideInput( label = "Per Mile", icon = painterResource(Res.drawable.ic_dollar_sign), - value = "0.50", + placeholder = "0.50", modifier = Modifier.weight(1f) ) } From 655a448535d796b917e9398519a80e0b075a9987 Mon Sep 17 00:00:00 2001 From: kennanLu Date: Wed, 26 Nov 2025 16:54:23 -0500 Subject: [PATCH 12/31] feature(rides): created new screen to track available ride offers after submission of a ride request --- .../feature/rides/AvailableRidesScreen.kt | 244 ++++++++++++++++++ .../example/demo/feature/rides/RideOption.kt | 14 + 2 files changed, 258 insertions(+) create mode 100644 app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt create mode 100644 app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/RideOption.kt diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt new file mode 100644 index 0000000..09fcf4e --- /dev/null +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt @@ -0,0 +1,244 @@ +package com.example.demo.feature.rides + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.composeapp.generated.resources.Res +import app.composeapp.generated.resources.ic_back_arrow +import com.example.demo.ui.theme.DrexelBlue +import com.example.demo.ui.theme.DrexelGold +import com.example.demo.ui.theme.HintGrey +import com.example.demo.ui.theme.FieldBackground +import org.jetbrains.compose.resources.painterResource + +@Composable +fun AvailableRidesScreen() { + // Dummy Data + val bestMatches = listOf( + RideOption("Abdul B.", 4.92, 11.71, 3, "Tesla Model Y (Blue)", "30th Street Station", "Cira Green", "Today 5:30 PM", true), + RideOption("Sarah M.", 4.87, 12.50, 2, "Honda Accord (Silver)", "30th Street Station", "Cira Green", "Today 5:45 PM", true), + RideOption("James K.", 4.85, 10.99, 4, "Toyota Camry (Black)", "30th Street Station", "Cira Green", "Today 6:00 PM", true) + ) + + val otherMatches = listOf( + RideOption("Lisa P.", 4.73, 14.25, 2, "Mazda CX-5 (Red)", "30th Street Station", "Cira Green", "Today 5:15 PM", false) + ) + + Column( + modifier = Modifier + .fillMaxSize() + .background(FieldBackground) + ) { + // Custom Header + Column( + modifier = Modifier + .fillMaxWidth() + .background(DrexelBlue) + .padding(start = 24.dp, end = 24.dp, top = 24.dp, bottom = 32.dp) + ) { + // Back Arrow and Filter Button + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton(onClick = { /* Handle Back */ }) { + Icon( + painter = painterResource(Res.drawable.ic_back_arrow), + contentDescription = "Back", + tint = Color.White + ) + } + + Button( + onClick = { /* Handle Filter */ }, + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1B4B6E)), // Slightly lighter blue + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + shape = RoundedCornerShape(20.dp) + ) { + Icon(painterResource(Res.drawable.ic_back_arrow), contentDescription = null, modifier = Modifier.size(16.dp)) + Spacer(modifier = Modifier.width(8.dp)) + Text("Filter") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Available Rides", + style = MaterialTheme.typography.headlineMedium, + color = Color.White, + fontWeight = FontWeight.Bold + ) + + Text( + text = "7 rides found for your route", + style = MaterialTheme.typography.titleMedium, + color = Color.White.copy(alpha = 0.7f) + ) + } + + // Scrollable List + LazyColumn( + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Best Matches + item { + SectionBadge(text = "Best Matches", color = DrexelGold, textColor = DrexelBlue) + Spacer(modifier = Modifier.height(8.dp)) + } + + items(bestMatches) { ride -> + RideCard(ride) + } + + // Other Matches + item { + Spacer(modifier = Modifier.height(16.dp)) + SectionBadge(text = "Other Matches", color = Color.White, textColor = HintGrey, isBordered = true) + Spacer(modifier = Modifier.height(8.dp)) + } + + items(otherMatches) { ride -> + RideCard(ride) + } + } + } +} + +@Composable +fun RideCard(ride: RideOption) { + Card( + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + // Name, Star, Price, and Seats + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = ride.driverName, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = DrexelBlue + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Icon( + painter = painterResource(Res.drawable.ic_back_arrow), + contentDescription = "Rating", + tint = DrexelGold, + modifier = Modifier.size(16.dp) + ) + + Text( + text = ride.rating.toString(), + style = MaterialTheme.typography.bodyMedium, + color = HintGrey, + modifier = Modifier.padding(start = 4.dp) + ) + } + + Column(horizontalAlignment = Alignment.End) { + Text( + text = "$${ride.price}", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = DrexelBlue + ) + Text( + text = "${ride.seats} seats", + style = MaterialTheme.typography.labelSmall, + color = HintGrey + ) + } + } + + // Car Model + Text( + text = ride.carModel, + style = MaterialTheme.typography.bodyMedium, + color = HintGrey + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Route + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = ride.pickup, + style = MaterialTheme.typography.bodyMedium, + color = HintGrey + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Icon( + painter = painterResource(Res.drawable.ic_back_arrow), + contentDescription = "to", + tint = HintGrey, + modifier = Modifier.size(14.dp) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = ride.dropoff, + style = MaterialTheme.typography.bodyMedium, + color = HintGrey + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Time + Text( + text = ride.time, + style = MaterialTheme.typography.bodyMedium, + color = DrexelBlue + ) + } + } +} + +@Composable +fun SectionBadge( + text: String, + color: Color, + textColor: Color, + isBordered: Boolean = false +) { + Surface( + color = color, + shape = RoundedCornerShape(50), + border = if (isBordered) null else null, + modifier = Modifier.wrapContentSize() + ) { + Text( + text = text, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + color = textColor + ) + } +} \ No newline at end of file diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/RideOption.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/RideOption.kt new file mode 100644 index 0000000..f50636d --- /dev/null +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/RideOption.kt @@ -0,0 +1,14 @@ +package com.example.demo.feature.rides + +// Simple data class to hold ride info +data class RideOption( + val driverName: String, + val rating: Double, + val price: Double, + val seats: Int, + val carModel: String, + val pickup: String, + val dropoff: String, + val time: String, + val isBestMatch: Boolean = false +) \ No newline at end of file From 7a6592d0c2cb4b7201c16535e4a95283c5c14329 Mon Sep 17 00:00:00 2001 From: kennanLu Date: Wed, 26 Nov 2025 16:54:38 -0500 Subject: [PATCH 13/31] feature(rides): created new screen to track available ride offers after submission of a ride request --- .../com/example/demo/feature/rides/AvailableRidesScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt index 09fcf4e..016a19e 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import app.composeapp.generated.resources.Res import app.composeapp.generated.resources.ic_back_arrow import com.example.demo.ui.theme.DrexelBlue From e865d13d10c2e94c9e1613a18ca77bf444b25027 Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Fri, 28 Nov 2025 17:31:43 -0500 Subject: [PATCH 14/31] feature(test_db): initial db test --- .../kotlin/com/example/demo/AndroidRideApp.kt | 112 ++++++++++++++++++ .../com/example/demo/RidShareDbHelper.kt | 29 +++++ .../kotlin/com/example/demo/RideRepository.kt | 51 ++++++++ 3 files changed, 192 insertions(+) create mode 100644 app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideApp.kt create mode 100644 app/composeApp/src/androidMain/kotlin/com/example/demo/RidShareDbHelper.kt create mode 100644 app/composeApp/src/androidMain/kotlin/com/example/demo/RideRepository.kt diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideApp.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideApp.kt new file mode 100644 index 0000000..cb6cb45 --- /dev/null +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideApp.kt @@ -0,0 +1,112 @@ +package com.example.demo + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp + +@Composable +fun AndroidRideApp() { + val context = LocalContext.current + val dbHelper = remember { RideShareDbHelper(context) } + val repo = remember { RideRepository(dbHelper) } + + var pickup by remember { mutableStateOf("") } + var dropoff by remember { mutableStateOf("") } + var time by remember { mutableStateOf("") } + + var rides by remember { mutableStateOf(emptyList()) } + + // Load rides on first launch + LaunchedEffect(Unit) { + rides = repo.getAllRides() + } + + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize() + ) { + Text( + text = "Ride Entry (Android + SQLite)", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(Modifier.height(16.dp)) + + OutlinedTextField( + value = pickup, + onValueChange = { pickup = it }, + label = { Text("Pickup Location") }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = dropoff, + onValueChange = { dropoff = it }, + label = { Text("Dropoff Location") }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = time, + onValueChange = { time = it }, + label = { Text("Time (e.g., 3:30 PM)") }, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(Modifier.height(8.dp)) + + Button( + onClick = { + if (pickup.isNotBlank() && dropoff.isNotBlank() && time.isNotBlank()) { + repo.insertRide(pickup, dropoff, time) + rides = repo.getAllRides() + pickup = "" + dropoff = "" + time = "" + } + } + ) { + Text("Save Ride") + } + + Spacer(Modifier.height(16.dp)) + + Text( + text = "Saved Rides:", + style = MaterialTheme.typography.titleMedium + ) + + Spacer(Modifier.height(8.dp)) + + LazyColumn( + modifier = Modifier.weight(1f) + ) { + items(rides) { ride -> + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + ) { + Text("${ride.pickup} → ${ride.dropoff}") + Text("Time: ${ride.time}", style = MaterialTheme.typography.bodySmall) + Text("ID: ${ride.id}", style = MaterialTheme.typography.bodySmall) + } + } + } + } + } +} diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/RidShareDbHelper.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/RidShareDbHelper.kt new file mode 100644 index 0000000..777c33d --- /dev/null +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/RidShareDbHelper.kt @@ -0,0 +1,29 @@ +package com.example.demo + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper + +class RideShareDbHelper(context: Context) : + SQLiteOpenHelper(context, "rideshare.db", null, 1) { + + override fun onCreate(db: SQLiteDatabase) { + // Create a super simple table for now + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS rides( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pickup TEXT NOT NULL, + dropoff TEXT NOT NULL, + ride_time TEXT NOT NULL + ); + """.trimIndent() + ) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + // Easiest upgrade strategy for school project + db.execSQL("DROP TABLE IF EXISTS rides;") + onCreate(db) + } +} diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/RideRepository.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/RideRepository.kt new file mode 100644 index 0000000..147d682 --- /dev/null +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/RideRepository.kt @@ -0,0 +1,51 @@ +package com.example.demo + +import android.content.ContentValues + +data class Ride( + val id: Long, + val pickup: String, + val dropoff: String, + val time: String +) + +class RideRepository(private val dbHelper: RideShareDbHelper) { + + fun insertRide(pickup: String, dropoff: String, time: String): Long { + val db = dbHelper.writableDatabase + val values = ContentValues().apply { + put("pickup", pickup) + put("dropoff", dropoff) + put("ride_time", time) + } + return db.insert("rides", null, values) + } + + fun getAllRides(): List { + val db = dbHelper.readableDatabase + val cursor = db.rawQuery( + "SELECT id, pickup, dropoff, ride_time FROM rides ORDER BY id DESC", + null + ) + + val rides = mutableListOf() + cursor.use { + val idxId = it.getColumnIndexOrThrow("id") + val idxPickup = it.getColumnIndexOrThrow("pickup") + val idxDropoff = it.getColumnIndexOrThrow("dropoff") + val idxTime = it.getColumnIndexOrThrow("ride_time") + + while (it.moveToNext()) { + rides.add( + Ride( + id = it.getLong(idxId), + pickup = it.getString(idxPickup), + dropoff = it.getString(idxDropoff), + time = it.getString(idxTime) + ) + ) + } + } + return rides + } +} From 86a192439085c5ca280c041bb35457ff9d7aa965 Mon Sep 17 00:00:00 2001 From: kennanLu Date: Sat, 29 Nov 2025 13:03:00 -0500 Subject: [PATCH 15/31] feature(rides): created new screen to track available matches with riders after submission of a ride offer --- .../com/example/demo/feature/rides/AvailableOffersScreen.kt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableOffersScreen.kt diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableOffersScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableOffersScreen.kt new file mode 100644 index 0000000..fe994e3 --- /dev/null +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableOffersScreen.kt @@ -0,0 +1,2 @@ +package com.example.demo.feature.rides + From 2d4492d0a5edeeaf90e565c9c331387010f221cc Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Sat, 29 Nov 2025 13:36:30 -0500 Subject: [PATCH 16/31] feature(db): pushing db to github --- .gitignore | 2 +- .../src/androidMain/assets/findmyride.db | Bin 0 -> 61440 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 app/composeApp/src/androidMain/assets/findmyride.db diff --git a/.gitignore b/.gitignore index 353c2ce..783ffbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -*.db + __pycache__/ *.log .env diff --git a/app/composeApp/src/androidMain/assets/findmyride.db b/app/composeApp/src/androidMain/assets/findmyride.db new file mode 100644 index 0000000000000000000000000000000000000000..4f09d7dada471dd7019c8ce03e87ca7a7f71eea1 GIT binary patch literal 61440 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 literal 0 HcmV?d00001 From feb15cf412c665ee73e5c14b9ee5acbfbcd08b59 Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Sat, 29 Nov 2025 14:32:59 -0500 Subject: [PATCH 17/31] feature(db): making db appear and connect to emulator --- .idea/deploymentTargetSelector.xml | 3 + .../src/androidMain/AndroidManifest.xml | 2 +- .../kotlin/com/example/demo/AndroidRideApp.kt | 112 ------------------ .../com/example/demo/AndroidRideRepository.kt | 87 ++++++++++++++ .../com/example/demo/FindMyRideDbProvider.kt | 42 +++++++ .../kotlin/com/example/demo/MainActivity.kt | 29 +++-- .../kotlin/com/example/demo/RideRepository.kt | 51 -------- ...dShareDbHelper.kt => RideShareDbHelper.kt} | 0 .../commonMain/kotlin/com/example/demo/App.kt | 7 +- .../example/demo/feature/db/RideRepository.kt | 24 ++++ .../example/demo/feature/main/MainRoute.kt | 5 +- 11 files changed, 182 insertions(+), 180 deletions(-) delete mode 100644 app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideApp.kt create mode 100644 app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideRepository.kt create mode 100644 app/composeApp/src/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt delete mode 100644 app/composeApp/src/androidMain/kotlin/com/example/demo/RideRepository.kt rename app/composeApp/src/androidMain/kotlin/com/example/demo/{RidShareDbHelper.kt => RideShareDbHelper.kt} (100%) create mode 100644 app/composeApp/src/commonMain/kotlin/com/example/demo/feature/db/RideRepository.kt diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 797d989..09a2ea2 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -30,6 +30,9 @@ + + \ No newline at end of file diff --git a/app/composeApp/src/androidMain/AndroidManifest.xml b/app/composeApp/src/androidMain/AndroidManifest.xml index cdba621..d973028 100644 --- a/app/composeApp/src/androidMain/AndroidManifest.xml +++ b/app/composeApp/src/androidMain/AndroidManifest.xml @@ -10,7 +10,7 @@ android:theme="@android:style/Theme.Material.Light.NoActionBar"> + android:name="com.example.rideshare.MainActivity"> diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideApp.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideApp.kt deleted file mode 100644 index cb6cb45..0000000 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideApp.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.example.demo - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp - -@Composable -fun AndroidRideApp() { - val context = LocalContext.current - val dbHelper = remember { RideShareDbHelper(context) } - val repo = remember { RideRepository(dbHelper) } - - var pickup by remember { mutableStateOf("") } - var dropoff by remember { mutableStateOf("") } - var time by remember { mutableStateOf("") } - - var rides by remember { mutableStateOf(emptyList()) } - - // Load rides on first launch - LaunchedEffect(Unit) { - rides = repo.getAllRides() - } - - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxSize() - ) { - Text( - text = "Ride Entry (Android + SQLite)", - style = MaterialTheme.typography.titleLarge - ) - - Spacer(Modifier.height(16.dp)) - - OutlinedTextField( - value = pickup, - onValueChange = { pickup = it }, - label = { Text("Pickup Location") }, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = dropoff, - onValueChange = { dropoff = it }, - label = { Text("Dropoff Location") }, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = time, - onValueChange = { time = it }, - label = { Text("Time (e.g., 3:30 PM)") }, - modifier = Modifier.fillMaxWidth() - ) - - Spacer(Modifier.height(8.dp)) - - Button( - onClick = { - if (pickup.isNotBlank() && dropoff.isNotBlank() && time.isNotBlank()) { - repo.insertRide(pickup, dropoff, time) - rides = repo.getAllRides() - pickup = "" - dropoff = "" - time = "" - } - } - ) { - Text("Save Ride") - } - - Spacer(Modifier.height(16.dp)) - - Text( - text = "Saved Rides:", - style = MaterialTheme.typography.titleMedium - ) - - Spacer(Modifier.height(8.dp)) - - LazyColumn( - modifier = Modifier.weight(1f) - ) { - items(rides) { ride -> - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - ) { - Text("${ride.pickup} → ${ride.dropoff}") - Text("Time: ${ride.time}", style = MaterialTheme.typography.bodySmall) - Text("ID: ${ride.id}", style = MaterialTheme.typography.bodySmall) - } - } - } - } - } -} diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideRepository.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideRepository.kt new file mode 100644 index 0000000..f3b3e22 --- /dev/null +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideRepository.kt @@ -0,0 +1,87 @@ +package com.example.demo + +import android.content.ContentValues +import com.example.demo.feature.db.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class AndroidRideRepository( + private val dbProvider: FindMyRideDbProvider +) : RideRepository { + + override suspend fun getOpenRideOffers(): List = + withContext(Dispatchers.IO) { + val db = dbProvider.getReadableDatabase() + + // Example: join RIDE_OFFER with LOCATION to get names + val sql = """ + SELECT + o.offer_id, + o.driver_id, + o.vehicle_id, + lo_from.name AS from_name, + lo_to.name AS to_name, + o.depart_at, + o.seats_available, + o.price_base + FROM RIDE_OFFER o + JOIN LOCATION lo_from ON o.original_location_id = lo_from.location_id + JOIN LOCATION lo_to ON o.dest_location_id = lo_to.location_id + WHERE o.status = 'open' + ORDER BY o.depart_at ASC; + """.trimIndent() + + val cursor = db.rawQuery(sql, null) + + val list = mutableListOf() + cursor.use { + val idxOfferId = it.getColumnIndexOrThrow("offer_id") + val idxDriverId = it.getColumnIndexOrThrow("driver_id") + val idxVehicleId = it.getColumnIndexOrThrow("vehicle_id") + val idxFromName = it.getColumnIndexOrThrow("from_name") + val idxToName = it.getColumnIndexOrThrow("to_name") + val idxDepartAt = it.getColumnIndexOrThrow("depart_at") + val idxSeatsAvail = it.getColumnIndexOrThrow("seats_available") + val idxPriceBase = it.getColumnIndexOrThrow("price_base") + + while (it.moveToNext()) { + list.add( + RideOffer( + offerId = it.getLong(idxOfferId), + driverId = it.getLong(idxDriverId), + vehicleId = it.getLong(idxVehicleId), + fromName = it.getString(idxFromName), + toName = it.getString(idxToName), + departAt = it.getString(idxDepartAt), + seatsAvailable = it.getInt(idxSeatsAvail), + priceBase = it.getDouble(idxPriceBase) + ) + ) + } + } + list + } + + override suspend fun createRideRequest( + riderId: Long, + pickupLocationId: Long, + dropoffLocationId: Long, + earliestPickup: String, + latestPickup: String?, + seatsNeeded: Int + ) { + withContext(Dispatchers.IO) { + val db = dbProvider.getWritableDatabase() + val values = ContentValues().apply { + put("rider_id", riderId) + put("pickup_location_id", pickupLocationId) + put("dropoff_location_id", dropoffLocationId) + put("earliest_pickup", earliestPickup) + put("latest_pickup", latestPickup) + put("seats_needed", seatsNeeded) + put("status", "open") + } + db.insert("RIDE_REQUEST", null, values) + } + } +} diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt new file mode 100644 index 0000000..17d2853 --- /dev/null +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt @@ -0,0 +1,42 @@ +package com.example.demo + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import java.io.FileOutputStream + +class FindMyRideDbProvider(private val context: Context) { + private val dbName = "findmyride.db" + + private fun ensureDbCopied() { + val dbFile = context.getDatabasePath(dbName) + if (dbFile.exists()) return + + dbFile.parentFile?.mkdirs() + + context.assets.open(dbName).use { input -> + FileOutputStream(dbFile).use { output -> + input.copyTo(output) + } + } + } + + fun getReadableDatabase(): SQLiteDatabase { + ensureDbCopied() + val dbFile = context.getDatabasePath(dbName) + return SQLiteDatabase.openDatabase( + dbFile.path, + null, + SQLiteDatabase.OPEN_READONLY + ) + } + + fun getWritableDatabase(): SQLiteDatabase { + ensureDbCopied() + val dbFile = context.getDatabasePath(dbName) + return SQLiteDatabase.openDatabase( + dbFile.path, + null, + SQLiteDatabase.OPEN_READONLY + ) + } +} \ No newline at end of file diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt index a3397f1..b303bdc 100644 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt @@ -1,26 +1,29 @@ -package com.example.app +package com.example.rideshare import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.example.demo.AndroidRideRepository import com.example.demo.App +import com.example.demo.FindMyRideDbProvider class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { - enableEdgeToEdge() super.onCreate(savedInstanceState) - setContent { - App() + MaterialTheme { + Surface { + val context = LocalContext.current + val repo = remember { + AndroidRideRepository(FindMyRideDbProvider(context)) + } + App(rideRepository = repo) + } + } } } } - -@Preview -@Composable -fun AppAndroidPreview() { - App() -} \ No newline at end of file diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/RideRepository.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/RideRepository.kt deleted file mode 100644 index 147d682..0000000 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/RideRepository.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.demo - -import android.content.ContentValues - -data class Ride( - val id: Long, - val pickup: String, - val dropoff: String, - val time: String -) - -class RideRepository(private val dbHelper: RideShareDbHelper) { - - fun insertRide(pickup: String, dropoff: String, time: String): Long { - val db = dbHelper.writableDatabase - val values = ContentValues().apply { - put("pickup", pickup) - put("dropoff", dropoff) - put("ride_time", time) - } - return db.insert("rides", null, values) - } - - fun getAllRides(): List { - val db = dbHelper.readableDatabase - val cursor = db.rawQuery( - "SELECT id, pickup, dropoff, ride_time FROM rides ORDER BY id DESC", - null - ) - - val rides = mutableListOf() - cursor.use { - val idxId = it.getColumnIndexOrThrow("id") - val idxPickup = it.getColumnIndexOrThrow("pickup") - val idxDropoff = it.getColumnIndexOrThrow("dropoff") - val idxTime = it.getColumnIndexOrThrow("ride_time") - - while (it.moveToNext()) { - rides.add( - Ride( - id = it.getLong(idxId), - pickup = it.getString(idxPickup), - dropoff = it.getString(idxDropoff), - time = it.getString(idxTime) - ) - ) - } - } - return rides - } -} diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/RidShareDbHelper.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/RideShareDbHelper.kt similarity index 100% rename from app/composeApp/src/androidMain/kotlin/com/example/demo/RidShareDbHelper.kt rename to app/composeApp/src/androidMain/kotlin/com/example/demo/RideShareDbHelper.kt diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt index 6ed3ec2..f354121 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.* import com.example.demo.feature.auth.login.LoginRoute import com.example.demo.feature.auth.signup.SignUpRoute import com.example.demo.feature.auth.forgot.ForgotPasswordRoute +import com.example.demo.feature.db.RideRepository import com.example.demo.feature.main.MainRoute import com.example.demo.feature.profile.ProfileRoute @@ -17,7 +18,7 @@ enum class RootScreen { } @Composable -fun App() { +fun App(rideRepository: RideRepository) { var currentScreen by remember { mutableStateOf(RootScreen.Login) } MaterialTheme { @@ -37,7 +38,9 @@ fun App() { onNavigateBack = { currentScreen = RootScreen.Login } ) - RootScreen.Main -> MainRoute() + RootScreen.Main -> MainRoute( + rideRepository = rideRepository + ) } } } diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/db/RideRepository.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/db/RideRepository.kt new file mode 100644 index 0000000..750c60d --- /dev/null +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/db/RideRepository.kt @@ -0,0 +1,24 @@ +package com.example.demo.feature.db + +data class RideOffer( + val offerId: Long, + val driverId: Long, + val vehicleId: Long, + val fromName: String, + val toName: String, + val departAt: String, + val seatsAvailable: Int, + val priceBase: Double +) + +interface RideRepository { + suspend fun getOpenRideOffers(): List + suspend fun createRideRequest( + riderId: Long, + pickupLocationId: Long, + dropoffLocationId: Long, + earliestPickup: String, + latestPickup: String?, + seatsNeeded: Int + ) +} diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt index c4f681b..5ceccc5 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.unit.dp import com.example.demo.feature.profile.ProfileRoute import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* +import com.example.demo.feature.db.RideRepository import com.example.demo.feature.messages.MessagesRoute import com.example.demo.ui.theme.DrexelBlue import com.example.demo.ui.theme.DrexelGold @@ -16,7 +17,9 @@ import com.example.demo.ui.theme.DrexelGold enum class MainTab { Home, Rides, Messages, Profile } @Composable -fun MainRoute() { +fun MainRoute( + rideRepository: RideRepository +) { var currentTab by remember { mutableStateOf(MainTab.Home) } // start on profile for now Scaffold( From fbdca6c4e9788b9f48f3b5ab384d6e70e0d1d993 Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Sat, 29 Nov 2025 14:55:24 -0500 Subject: [PATCH 18/31] feature(auth_db_connect): getting the auth connection to db --- .../com/example/demo/AndroidAuthRepository.kt | 98 +++++++++++++++++++ .../com/example/demo/FindMyRideDbProvider.kt | 2 +- 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidAuthRepository.kt diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidAuthRepository.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidAuthRepository.kt new file mode 100644 index 0000000..c5a7a3d --- /dev/null +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidAuthRepository.kt @@ -0,0 +1,98 @@ +package com.example.demo + +import com.example.demo.feature.auth.data.AuthRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Android implementation of AuthRepository that talks to the findmyride.db SQLite database. + */ +class AndroidAuthRepository( + private val dbProvider: FindMyRideDbProvider +) : AuthRepository { + + override suspend fun login(email: String, password: String): Result = + withContext(Dispatchers.IO) { + val db = dbProvider.getReadableDatabase() + + val cursor = db.rawQuery( + """ + SELECT user_id + FROM "USER" + WHERE email = ? AND password_hash = ? + LIMIT 1; + """.trimIndent(), + arrayOf(email, password) + ) + + cursor.use { + return@withContext if (it.moveToFirst()) { + Result.success(Unit) + } else { + Result.failure(IllegalArgumentException("Invalid email or password")) + } + } + } + + // Basic implementation so Sign Up / Forgot Password don't crash, + // we can improve these later. + override suspend fun signUp( + fullName: String, + email: String, + password: String + ): Result = + withContext(Dispatchers.IO) { + val db = dbProvider.getWritableDatabase() + + // Check if email already exists + db.rawQuery( + """SELECT 1 FROM "USER" WHERE email = ? LIMIT 1;""", + arrayOf(email) + ).use { c -> + if (c.moveToFirst()) { + return@withContext Result.failure( + IllegalArgumentException("Email already registered") + ) + } + } + + val username = if (fullName.isNotBlank()) { + fullName.trim().lowercase().replace(" ", "_") + } else { + email.substringBefore('@') + } + + val stmt = db.compileStatement( + """ + INSERT INTO "USER"(email, username, password_hash, role) + VALUES(?, ?, ?, 'rider'); + """.trimIndent() + ) + stmt.bindString(1, email) + stmt.bindString(2, username) + stmt.bindString(3, password) + + val rowId = stmt.executeInsert() + if (rowId == -1L) { + Result.failure(IllegalStateException("Failed to create account")) + } else { + Result.success(Unit) + } + } + + override suspend fun sendPasswordReset(email: String): Result = + withContext(Dispatchers.IO) { + val db = dbProvider.getReadableDatabase() + db.rawQuery( + """SELECT 1 FROM "USER" WHERE email = ? LIMIT 1;""", + arrayOf(email) + ).use { c -> + return@withContext if (c.moveToFirst()) { + // Pretend an email was sent + Result.success(Unit) + } else { + Result.failure(IllegalArgumentException("No account found for this email")) + } + } + } +} diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt index 17d2853..3da45bc 100644 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt @@ -36,7 +36,7 @@ class FindMyRideDbProvider(private val context: Context) { return SQLiteDatabase.openDatabase( dbFile.path, null, - SQLiteDatabase.OPEN_READONLY + SQLiteDatabase.OPEN_READWRITE ) } } \ No newline at end of file From 1c1697a4bab098204b82238088fcc9bf0184c0d9 Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Sat, 29 Nov 2025 18:02:26 -0500 Subject: [PATCH 19/31] feature(db): populate db --- .../src/androidMain/assets/findmyride.db | Bin 61440 -> 106496 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/composeApp/src/androidMain/assets/findmyride.db b/app/composeApp/src/androidMain/assets/findmyride.db index 4f09d7dada471dd7019c8ce03e87ca7a7f71eea1..cccab052394565b1e14aca5b3979a71cd5d6787a 100644 GIT binary patch literal 106496 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

;qKr+3aBGs2o}!uxOm3>|B={#sY`t}v7Q)? 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 From 4995d3224526def02f6106cacdb0b58f26cc11ae Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Sun, 30 Nov 2025 12:51:22 -0500 Subject: [PATCH 20/31] feature(db): connect login with db --- .../com/example/demo/AndroidAuthRepository.kt | 22 ++++++- .../demo/feature/auth/login/LoginRoute.kt | 7 ++- .../demo/feature/auth/login/LoginViewModel.kt | 57 +++++++++++-------- 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidAuthRepository.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidAuthRepository.kt index c5a7a3d..8e74451 100644 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidAuthRepository.kt +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidAuthRepository.kt @@ -1,5 +1,6 @@ package com.example.demo +import app.composeapp.generated.resources.Res import com.example.demo.feature.auth.data.AuthRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -11,24 +12,41 @@ class AndroidAuthRepository( private val dbProvider: FindMyRideDbProvider ) : AuthRepository { + override suspend fun login(email: String, password: String): Result = withContext(Dispatchers.IO) { val db = dbProvider.getReadableDatabase() val cursor = db.rawQuery( """ - SELECT user_id + SELECT user_id, email, username, role FROM "USER" WHERE email = ? AND password_hash = ? - LIMIT 1; """.trimIndent(), arrayOf(email, password) ) cursor.use { return@withContext if (it.moveToFirst()) { + val idxId = it.getColumnIndexOrThrow("user_id") + val idxEmail = it.getColumnIndexOrThrow("email") + val idxUsername = it.getColumnIndexOrThrow("username") + val idxRole = it.getColumnIndexOrThrow("role") + + // Remember who is logged in + CurrentUserStore.userId = it.getLong(idxId) + CurrentUserStore.email = it.getString(idxEmail) + CurrentUserStore.username = it.getString(idxUsername) + CurrentUserStore.role = it.getString(idxRole) + Result.success(Unit) } else { + // Clear any previous user + CurrentUserStore.userId = null + CurrentUserStore.email = null + CurrentUserStore.username = null + CurrentUserStore.role = null + Result.failure(IllegalArgumentException("Invalid email or password")) } } diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/auth/login/LoginRoute.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/auth/login/LoginRoute.kt index 52cd63f..a75e553 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/auth/login/LoginRoute.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/auth/login/LoginRoute.kt @@ -1,15 +1,20 @@ package com.example.demo.feature.auth.login +import FakeAuthRepository import androidx.compose.runtime.* import androidx.compose.runtime.collectAsState +import com.example.demo.feature.auth.data.AuthRepository @Composable fun LoginRoute( onNavigateToSignUp: () -> Unit, onNavigateToForgotPassword: () -> Unit, onLoginSuccess: () -> Unit, - viewModel: LoginViewModel = remember { LoginViewModel() } + authRepository: AuthRepository ) { + val viewModel = remember(authRepository) { + LoginViewModel(authRepository) + } val state by viewModel.uiState.collectAsState() // When login succeeds, trigger navigation once diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/auth/login/LoginViewModel.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/auth/login/LoginViewModel.kt index 3f0b082..7ec7094 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/auth/login/LoginViewModel.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/auth/login/LoginViewModel.kt @@ -1,6 +1,5 @@ package com.example.demo.feature.auth.login -import FakeAuthRepository import com.example.demo.feature.auth.data.AuthRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -10,7 +9,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch class LoginViewModel( - private val repository: AuthRepository = FakeAuthRepository() + private val repository: AuthRepository ) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -19,11 +18,21 @@ class LoginViewModel( fun onEvent(event: LoginEvent) { when (event) { - is LoginEvent.EmailChanged -> - _uiState.value = _uiState.value.copy(email = event.value, errorMessage = null) + is LoginEvent.EmailChanged -> { + _uiState.value = _uiState.value.copy( + email = event.value, + errorMessage = null, + loginSuccess = false + ) + } - is LoginEvent.PasswordChanged -> - _uiState.value = _uiState.value.copy(password = event.value, errorMessage = null) + is LoginEvent.PasswordChanged -> { + _uiState.value = _uiState.value.copy( + password = event.value, + errorMessage = null, + loginSuccess = false + ) + } LoginEvent.Submit -> submit() } @@ -31,36 +40,37 @@ class LoginViewModel( private fun submit() { val state = _uiState.value - val email = state.email.trim() - val password = state.password - // Required fields - if (email.isBlank() || password.isBlank()) { - return setError("Email and password are required.") + if (!isValidEmail(state.email)) { + setError("Please enter a valid email") + return } - - // Basic email validation - if (!isValidEmail(email)) { - return setError("Please enter a valid email address.") + if (state.password.isBlank()) { + setError("Password is required") + return } + // start loading _uiState.value = state.copy( isLoading = true, - errorMessage = null + errorMessage = null, + loginSuccess = false ) scope.launch { - val result = repository.login(email, password) + val result = repository.login(state.email.trim(), state.password) - if (result.isSuccess) { - _uiState.value = _uiState.value.copy( + _uiState.value = if (result.isSuccess) { + _uiState.value.copy( isLoading = false, loginSuccess = true, + errorMessage = null ) } else { - _uiState.value = _uiState.value.copy( + _uiState.value.copy( isLoading = false, - errorMessage = result.exceptionOrNull()?.message ?: "Login failed." + loginSuccess = false, + errorMessage = result.exceptionOrNull()?.message ?: "Login failed" ) } } @@ -69,14 +79,15 @@ class LoginViewModel( private fun setError(message: String) { _uiState.value = _uiState.value.copy( errorMessage = message, - isLoading = false + isLoading = false, + loginSuccess = false ) } + // Same simple cross-platform email validation you used before private fun isValidEmail(email: String): Boolean { val at = email.indexOf('@') val dot = email.lastIndexOf('.') return at > 0 && dot > at + 1 && dot < email.length - 1 } } - From 35835bfb7aa348500706aad56301047a1ab19122 Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Sun, 30 Nov 2025 12:55:26 -0500 Subject: [PATCH 21/31] feature(db): connect profile page with db --- .../example/demo/AndroidProfileRepository.kt | 197 ++++++++++++++++++ .../com/example/demo/CurrentUserStore.kt | 14 ++ .../demo/feature/profile/ProfileScreen.kt | 39 ++-- 3 files changed, 232 insertions(+), 18 deletions(-) create mode 100644 app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidProfileRepository.kt create mode 100644 app/composeApp/src/androidMain/kotlin/com/example/demo/CurrentUserStore.kt diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidProfileRepository.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidProfileRepository.kt new file mode 100644 index 0000000..1c9ba60 --- /dev/null +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidProfileRepository.kt @@ -0,0 +1,197 @@ +package com.example.demo.feature.profile.data + +import com.example.demo.CurrentUserStore +import com.example.demo.FindMyRideDbProvider +import com.example.demo.feature.profile.ProfileUiState +import com.example.demo.feature.profile.VehicleUi + +/** + * Android implementation of ProfileRepository that reads/writes + * the USER and VEHICLE tables from findmyride.db + */ +class AndroidProfileRepository( + private val dbProvider: FindMyRideDbProvider +) : ProfileRepository { + + private fun requireCurrentUserId(): Long { + return CurrentUserStore.userId + ?: error("No logged-in user. Make sure login ran successfully before opening Profile.") + } + + override fun loadInitialProfile(): ProfileUiState { + val currentUserId = requireCurrentUserId() + val db = dbProvider.getReadableDatabase() + + // ----- USER ----- + var name = "Unknown" + var email = "" + var phone = "" + var rating = 0.0 + + db.rawQuery( + """ + SELECT username, email, phone_number, rating_avg + FROM "USER" + WHERE user_id = ? + LIMIT 1; + """.trimIndent(), + arrayOf(currentUserId.toString()) + ).use { c -> + if (c.moveToFirst()) { + name = c.getString(c.getColumnIndexOrThrow("username")) + email = c.getString(c.getColumnIndexOrThrow("email")) + phone = c.getString(c.getColumnIndexOrThrow("phone_number")) ?: "" + rating = c.getDouble(c.getColumnIndexOrThrow("rating_avg")) + } + } + + // ----- VEHICLES ----- + val vehicles = mutableListOf() + db.rawQuery( + """ + SELECT vehicle_id, make, model, color, plate, seats_total, year, fun_fact + FROM "VEHICLE" + WHERE owner_user_id = ? + ORDER BY vehicle_id; + """.trimIndent(), + arrayOf(currentUserId.toString()) + ).use { c -> + val idxId = c.getColumnIndexOrThrow("vehicle_id") + val idxMake = c.getColumnIndexOrThrow("make") + val idxModel = c.getColumnIndexOrThrow("model") + val idxColor = c.getColumnIndexOrThrow("color") + val idxPlate = c.getColumnIndexOrThrow("plate") + val idxSeats = c.getColumnIndexOrThrow("seats_total") + val idxYear = c.getColumnIndexOrThrow("year") + val idxFunFact = c.getColumnIndexOrThrow("fun_fact") + + while (c.moveToNext()) { + vehicles += VehicleUi( + id = c.getInt(idxId), + ownerUserId = currentUserId.toInt(), + make = c.getString(idxMake), + model = c.getString(idxModel), + color = c.getString(idxColor) ?: "", + plate = c.getString(idxPlate), + seatsTotal = c.getInt(idxSeats), + year = c.getInt(idxYear), + funFact = c.getString(idxFunFact) ?: "" + ) + } + } + + // Build ProfileUiState using DB values + return ProfileUiState( + name = name, + email = email, + rating = rating, + vehicles = vehicles, + account = ProfileUiState().account.copy( + fullName = name, + email = email, + phone = phone, + // we don't show real password here + password = "*******" + ) + ) + } + + override fun saveProfile(state: ProfileUiState) { + val currentUserId = requireCurrentUserId() + val db = dbProvider.getWritableDatabase() + db.beginTransaction() + try { + // ----- UPDATE USER ----- + db.compileStatement( + """ + UPDATE "USER" + SET username = ?, email = ?, phone_number = ? + WHERE user_id = ?; + """.trimIndent() + ).apply { + bindString(1, state.account.fullName) + bindString(2, state.account.email) + bindString(3, state.account.phone) + bindLong(4, currentUserId) + executeUpdateDelete() + } + + // ----- SYNC VEHICLES ----- + // Get existing vehicle ids in DB + val existingIds = mutableSetOf() + db.rawQuery( + """ + SELECT vehicle_id + FROM "VEHICLE" + WHERE owner_user_id = ?; + """.trimIndent(), + arrayOf(currentUserId.toString()) + ).use { c -> + val idx = c.getColumnIndexOrThrow("vehicle_id") + while (c.moveToNext()) existingIds += c.getInt(idx) + } + + val desiredIds = state.vehicles.map { it.id }.toSet() + + // Upsert each vehicle from UI state + state.vehicles.forEach { v -> + if (existingIds.contains(v.id)) { + // UPDATE + db.compileStatement( + """ + UPDATE "VEHICLE" + SET make = ?, model = ?, color = ?, plate = ?, + seats_total = ?, year = ?, fun_fact = ? + WHERE vehicle_id = ?; + """.trimIndent() + ).apply { + bindString(1, v.make) + bindString(2, v.model) + bindString(3, v.color) + bindString(4, v.plate) + bindLong(5, v.seatsTotal.toLong()) + bindLong(6, v.year.toLong()) + bindString(7, v.funFact) + bindLong(8, v.id.toLong()) + executeUpdateDelete() + } + } else { + // INSERT with explicit vehicle_id (matches the id created in ViewModel) + db.compileStatement( + """ + INSERT INTO "VEHICLE"( + vehicle_id, owner_user_id, make, model, color, + plate, seats_total, year, fun_fact + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); + """.trimIndent() + ).apply { + bindLong(1, v.id.toLong()) + bindLong(2, currentUserId) + bindString(3, v.make) + bindString(4, v.model) + bindString(5, v.color) + bindString(6, v.plate) + bindLong(7, v.seatsTotal.toLong()) + bindLong(8, v.year.toLong()) + bindString(9, v.funFact) + executeInsert() + } + } + } + + // Delete vehicles that were removed in the UI + (existingIds - desiredIds).forEach { idToDelete -> + db.compileStatement( + """DELETE FROM "VEHICLE" WHERE vehicle_id = ?;""".trimIndent() + ).apply { + bindLong(1, idToDelete.toLong()) + executeUpdateDelete() + } + } + + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } +} diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/CurrentUserStore.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/CurrentUserStore.kt new file mode 100644 index 0000000..cf175de --- /dev/null +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/CurrentUserStore.kt @@ -0,0 +1,14 @@ +package com.example.demo + +/** + * Simple in-memory store for the currently logged-in user. + * Android only; commonMain doesn't see this directly. + */ +object CurrentUserStore { + var userId: Long? = null + var email: String? = null + var username: String? = null + var role: String? = null +} + +// This is just a global holder on the Android side. \ No newline at end of file diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/profile/ProfileScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/profile/ProfileScreen.kt index c19250a..58ba576 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/profile/ProfileScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/profile/ProfileScreen.kt @@ -20,6 +20,8 @@ import androidx.compose.ui.unit.sp import com.example.demo.feature.profile.components.SettingsCard import com.example.demo.feature.profile.components.VehicleEditDialog import com.example.demo.feature.profile.components.VehiclesCard +import com.example.demo.feature.profile.data.InMemoryProfileRepository +import com.example.demo.feature.profile.data.ProfileRepository import com.example.demo.feature.profile.pages.AccountSettingsScreen import com.example.demo.feature.profile.pages.PreferencesScreen import com.example.demo.feature.profile.pages.PrivacySafetyScreen @@ -35,9 +37,10 @@ enum class ProfilePage { @Composable fun ProfileRoute( - viewModel: ProfileViewModel = remember { ProfileViewModel() }, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + repository: ProfileRepository = InMemoryProfileRepository() ) { + val viewModel = remember(repository) { ProfileViewModel(repository) } var currentPage by remember { mutableStateOf(ProfilePage.Overview) } val state by viewModel.uiState.collectAsState() @@ -46,35 +49,36 @@ fun ProfileRoute( state = state, onEvent = viewModel::onEvent, onAccountSettingsClick = { currentPage = ProfilePage.AccountSettings }, - onPreferencesClick = { currentPage = ProfilePage.Preferences }, - onPrivacySafetyClick = { currentPage = ProfilePage.PrivacySafety }, + onPreferencesClick = { currentPage = ProfilePage.Preferences }, + onPrivacySafetyClick = { currentPage = ProfilePage.PrivacySafety }, modifier = modifier ) ProfilePage.AccountSettings -> AccountSettingsScreen( state = state.account, onChange = { updated -> - viewModel.onEvent(ProfileEvent.AccountSettingsChanged( - fullName = updated.fullName, - email = updated.email, - phone = updated.phone, - password = updated.password - )) + viewModel.onEvent( + ProfileEvent.AccountSettingsChanged( + fullName = updated.fullName, + email = updated.email, + phone = updated.phone, + password = updated.password + ) + ) }, onSave = { viewModel.onEvent(ProfileEvent.SaveAccountSettings) }, onDelete = { viewModel.onEvent(ProfileEvent.DeleteAccount) }, onBack = { currentPage = ProfilePage.Overview } ) - ProfilePage.Preferences -> PreferencesScreen( prefs = state.preferences, onChange = { newPrefs -> viewModel.onEvent( ProfileEvent.PreferencesChanged( - notificationsEnabled = newPrefs.notificationsEnabled, - emailUpdatesEnabled = newPrefs.emailUpdatesEnabled, - darkModeEnabled = newPrefs.darkModeEnabled + notificationsEnabled = newPrefs.notificationsEnabled, + emailUpdatesEnabled = newPrefs.emailUpdatesEnabled, + darkModeEnabled = newPrefs.darkModeEnabled ) ) }, @@ -88,10 +92,10 @@ fun ProfileRoute( onChange = { updated -> viewModel.onEvent( ProfileEvent.PrivacySafetyChanged( - showProfilePublicly = updated.showProfilePublicly, + showProfilePublicly = updated.showProfilePublicly, allowMessagesFromNonContacts = updated.allowMessagesFromNonContacts, - shareTripHistoryWithFriends = updated.shareTripHistoryWithFriends, - twoFactorEnabled = updated.twoFactorEnabled + shareTripHistoryWithFriends = updated.shareTripHistoryWithFriends, + twoFactorEnabled = updated.twoFactorEnabled ) ) }, @@ -99,7 +103,6 @@ fun ProfileRoute( onBack = { currentPage = ProfilePage.Overview }, modifier = modifier ) - } } From 150c45c7d3f357c9e1cee6450a12208ed46f1923 Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Sun, 30 Nov 2025 12:55:45 -0500 Subject: [PATCH 22/31] feature(db): update db with MESSAGES and MESSAGE_THREAD --- .../src/androidMain/assets/findmyride.db | Bin 106496 -> 114688 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/composeApp/src/androidMain/assets/findmyride.db b/app/composeApp/src/androidMain/assets/findmyride.db index cccab052394565b1e14aca5b3979a71cd5d6787a..08fc017a259c4982323501629aaa54e7b379de45 100644 GIT binary patch delta 1349 zcmZoTz}C>fK0#WrjDdkc28dyRbE1whYZ-%HS?9)-1^0#6nHm{*u5*=e*|UFSKf*Gf zsd4i|g>0tS7HK|qacODB4&IW)q?}Y=*Wh4BcULGS;~eDb7~-lB;^^e#s-OfFRZ`I4 z6a#M?o6Vp@UGgBr9vWP0WHBvzX;$EQFG!=qe-CToQ{hVEc z;UNvBnb2 zEd(M98jWy&f%ub4*+ub(2~<~UacYqvFfbroPrndXch?{VKmQN~zfd0^{9Z>g!U&5I z^(2^4l$nwmpPN{coB=Wj>I@wPU0sFzf|AVqyu=)ZoXoszg_3-QAWs+9cwfg5XAivb zqM*PJc3(0ub(W;2fYQnWP8}-(dcayxk_#+=G$+5|R3uM3C~&~q?Mb#BC0sNohrCpF zCdLeB4_9Yz1r3RJjKy0u>fGKmbA*0^rPtQNNGcs>~tj{Q0z{LwJa~S!bFz`Rw zE||a=!as3An}!r48v`dJgE6y_fsv`Mp`os^fr5dt6%d)2fYdYbw=(b_;ctcMP_4FQ z0_g}gW{ylP)=}_OD9_BvQAkQvD9K1IN>#|rQ!q48$j!_vElDlb!!*>$3S=Rtzp+PZ irH(=(P)B*HLU9SmjQl)>wEQ9kQ!Aho0(|YU=mh|F#w~^b delta 78 zcmZo@U~f3UHbGjjkb!|g3W#BVeWH#rYaxSPm&nGH1^4+_*^e;rT<0p`vS Date: Sun, 30 Nov 2025 12:56:07 -0500 Subject: [PATCH 23/31] feature(db): connect messages with db --- .../example/demo/AndroidMessagesRepository.kt | 173 ++++++++++++++++++ .../demo/feature/messages/MessagesRoute.kt | 22 ++- .../feature/messages/chat/ChatDetailRoute.kt | 10 +- .../messages/chat/ChatDetailViewModel.kt | 49 +++-- .../messages/list/MessagesListRoute.kt | 9 +- 5 files changed, 235 insertions(+), 28 deletions(-) create mode 100644 app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidMessagesRepository.kt diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidMessagesRepository.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidMessagesRepository.kt new file mode 100644 index 0000000..6d26d58 --- /dev/null +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidMessagesRepository.kt @@ -0,0 +1,173 @@ +package com.example.demo.feature.messages.data + +import com.example.demo.CurrentUserStore +import com.example.demo.FindMyRideDbProvider +import com.example.demo.feature.messages.chat.ChatMessageUi +import com.example.demo.feature.messages.list.MessageThreadUi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class AndroidMessagesRepository( + private val dbProvider: FindMyRideDbProvider +) : MessagesRepository { + + /** + * Use logged-in user if available, otherwise fall back to parameter. + */ + private fun resolveCurrentUserId(paramUserId: Int): Int { + val stored = CurrentUserStore.userId + return stored?.toInt() ?: paramUserId + } + + override suspend fun getThreadsForUser(userId: Int): List = + withContext(Dispatchers.IO) { + val db = dbProvider.getReadableDatabase() + val currentUserId = resolveCurrentUserId(userId) + + val sql = """ + SELECT + t.thread_id, + CASE + WHEN t.user1_id = ? THEN u2.username + ELSE u1.username + END AS contact_name, + last_msg.body AS last_message, + last_msg.sent_at AS last_sent_at + FROM MESSAGE_THREAD t + JOIN "USER" u1 ON t.user1_id = u1.user_id + JOIN "USER" u2 ON t.user2_id = u2.user_id + LEFT JOIN MESSAGE last_msg ON last_msg.message_id = ( + SELECT m.message_id + FROM MESSAGE m + WHERE m.thread_id = t.thread_id + ORDER BY m.sent_at DESC, m.message_id DESC + LIMIT 1 + ) + WHERE t.user1_id = ? OR t.user2_id = ? + ORDER BY last_sent_at DESC NULLS LAST, t.thread_id DESC; + """.trimIndent() + + val args = arrayOf( + currentUserId.toString(), + currentUserId.toString(), + currentUserId.toString() + ) + + val cursor = db.rawQuery(sql, args) + val result = mutableListOf() + + cursor.use { c -> + val idxThreadId = c.getColumnIndexOrThrow("thread_id") + val idxName = c.getColumnIndexOrThrow("contact_name") + val idxLastMsg = c.getColumnIndexOrThrow("last_message") + val idxLastSentAt = c.getColumnIndexOrThrow("last_sent_at") + + while (c.moveToNext()) { + val name = c.getString(idxName) + val lastMsg = c.getString(idxLastMsg) ?: "No messages yet" + val sentAt = c.getString(idxLastSentAt) ?: "" + + result += MessageThreadUi( + id = c.getInt(idxThreadId), + senderName = name, + initials = initialsFromName(name), + lastMessage = lastMsg, + timeAgo = sentAt, // keep it simple: show timestamp string + unreadCount = 0 + ) + } + } + + result + } + + override suspend fun getMessagesForThread(threadId: Int): List = + withContext(Dispatchers.IO) { + val db = dbProvider.getReadableDatabase() + val currentUserId = resolveCurrentUserId(1) + + val cursor = db.rawQuery( + """ + SELECT message_id, sender_id, body, sent_at + FROM MESSAGE + WHERE thread_id = ? + ORDER BY sent_at ASC, message_id ASC; + """.trimIndent(), + arrayOf(threadId.toString()) + ) + + val result = mutableListOf() + cursor.use { c -> + val idxId = c.getColumnIndexOrThrow("message_id") + val idxSender= c.getColumnIndexOrThrow("sender_id") + val idxBody = c.getColumnIndexOrThrow("body") + val idxTime = c.getColumnIndexOrThrow("sent_at") + + while (c.moveToNext()) { + val senderId = c.getInt(idxSender) + result += ChatMessageUi( + id = c.getInt(idxId), + isMe = (senderId == currentUserId), + text = c.getString(idxBody), + time = c.getString(idxTime) + ) + } + } + result + } + + override suspend fun sendMessage(threadId: Int, text: String): ChatMessageUi = + withContext(Dispatchers.IO) { + val db = dbProvider.getWritableDatabase() + val currentUserId = resolveCurrentUserId(1) + + // Insert message + val insertStmt = db.compileStatement( + """ + INSERT INTO MESSAGE(thread_id, sender_id, body) + VALUES (?, ?, ?); + """.trimIndent() + ) + insertStmt.bindLong(1, threadId.toLong()) + insertStmt.bindLong(2, currentUserId.toLong()) + insertStmt.bindString(3, text) + val newIdLong = insertStmt.executeInsert() + + // Get timestamp (sent_at) + val cursor = db.rawQuery( + """ + SELECT sent_at + FROM MESSAGE + WHERE message_id = ?; + """.trimIndent(), + arrayOf(newIdLong.toString()) + ) + + var sentAt = "Now" + cursor.use { c -> + if (c.moveToFirst()) { + sentAt = c.getString(c.getColumnIndexOrThrow("sent_at")) + } + } + + ChatMessageUi( + id = newIdLong.toInt(), + isMe = true, + text = text, + time = sentAt + ) + } + + private fun initialsFromName(name: String): String { + val parts = name.trim().split(" ") + .filter { it.isNotBlank() } + + val chars = when { + parts.isEmpty() -> listOf('U') + parts.size == 1 -> listOf(parts[0].first()) + else -> listOf(parts[0].first(), parts[1].first()) + } + + return chars.joinToString("").uppercase() + } +} diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/MessagesRoute.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/MessagesRoute.kt index a014cf3..7c1c121 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/MessagesRoute.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/MessagesRoute.kt @@ -3,6 +3,7 @@ package com.example.demo.feature.messages import androidx.compose.runtime.* import androidx.compose.ui.Modifier import com.example.demo.feature.messages.chat.ChatDetailRoute +import com.example.demo.feature.messages.data.MessagesRepository import com.example.demo.feature.messages.list.MessageThreadUi import com.example.demo.feature.messages.list.MessagesListRoute @@ -10,19 +11,23 @@ private enum class MessagesPage { List, Conversation } @Composable fun MessagesRoute( - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + repository: MessagesRepository ) { var currentPage by remember { mutableStateOf(MessagesPage.List) } var activeThread by remember { mutableStateOf(null) } when (currentPage) { - MessagesPage.List -> MessagesListRoute( - modifier = modifier, - onOpenConversation = { thread -> - activeThread = thread - currentPage = MessagesPage.Conversation - } - ) + MessagesPage.List -> { + MessagesListRoute( + modifier = modifier, + repository = repository, + onOpenConversation = { thread -> + activeThread = thread + currentPage = MessagesPage.Conversation + } + ) + } MessagesPage.Conversation -> { val thread = activeThread @@ -33,6 +38,7 @@ fun MessagesRoute( threadId = thread.id, contactName = thread.senderName, initials = thread.initials, + repository = repository, onBack = { currentPage = MessagesPage.List }, modifier = modifier ) diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/chat/ChatDetailRoute.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/chat/ChatDetailRoute.kt index 54ad70a..aa6ed9f 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/chat/ChatDetailRoute.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/chat/ChatDetailRoute.kt @@ -2,18 +2,20 @@ package com.example.demo.feature.messages.chat import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import com.example.demo.feature.messages.data.MessagesRepository @Composable fun ChatDetailRoute( threadId: Int, contactName: String, initials: String, + repository: MessagesRepository, onBack: () -> Unit, - modifier: Modifier = Modifier, - viewModel: ChatDetailViewModel = remember { - ChatDetailViewModel(threadId, contactName, initials) - } + modifier: Modifier = Modifier ) { + val viewModel = remember(threadId, repository) { + ChatDetailViewModel(threadId, contactName, initials, repository) + } val state by viewModel.uiState.collectAsState() ChatDetailScreen( diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/chat/ChatDetailViewModel.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/chat/ChatDetailViewModel.kt index fb650ea..86eebad 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/chat/ChatDetailViewModel.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/chat/ChatDetailViewModel.kt @@ -1,5 +1,6 @@ package com.example.demo.feature.messages.chat +import com.example.demo.feature.messages.data.MessagesRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -12,6 +13,7 @@ class ChatDetailViewModel( private val threadId: Int, contactName: String, initials: String, + private val repository: MessagesRepository ) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -30,15 +32,22 @@ class ChatDetailViewModel( private fun loadMessages() { scope.launch { - // fake loading – later replace with repository - _uiState.update { - it.copy( - isLoading = false, - messages = listOf( - ChatMessageUi(1, false, "Hey!", "3h ago"), - ChatMessageUi(2, true, "Yo", "3h ago"), + try { + val msgs = repository.getMessagesForThread(threadId) + _uiState.update { + it.copy( + isLoading = false, + messages = msgs, + errorMessage = null ) - ) + } + } catch (e: Exception) { + _uiState.update { + it.copy( + isLoading = false, + errorMessage = e.message ?: "Failed to load messages" + ) + } } } } @@ -56,13 +65,23 @@ class ChatDetailViewModel( val text = _uiState.value.newMessageText.trim() if (text.isBlank()) return - val newId = (_uiState.value.messages.maxOfOrNull { it.id } ?: 0) + 1 - - _uiState.update { - it.copy( - messages = it.messages + ChatMessageUi(newId, true, text, "Now"), - newMessageText = "" - ) + scope.launch { + try { + val msg = repository.sendMessage(threadId, text) + _uiState.update { + it.copy( + messages = it.messages + msg, + newMessageText = "", + errorMessage = null + ) + } + } catch (e: Exception) { + _uiState.update { + it.copy( + errorMessage = e.message ?: "Failed to send message" + ) + } + } } } } diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/list/MessagesListRoute.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/list/MessagesListRoute.kt index 55b6164..37eb88e 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/list/MessagesListRoute.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/list/MessagesListRoute.kt @@ -2,13 +2,20 @@ package com.example.demo.feature.messages.list import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import com.example.demo.feature.messages.data.FakeMessagesRepository +import com.example.demo.feature.messages.data.MessagesRepository @Composable fun MessagesListRoute( modifier: Modifier = Modifier, onOpenConversation: (MessageThreadUi) -> Unit, - viewModel: MessagesViewModel = remember { MessagesViewModel() } + repository: MessagesRepository = FakeMessagesRepository() ) { + val viewModel = remember(repository) { + // userId is still 1 here; Android repo will ignore it and use CurrentUserStore + MessagesViewModel(repository = repository, userId = 1) + } + val state by viewModel.uiState.collectAsState() MessagesScreen( From 0931a238c353b6a341d21fb60dcda43450bace6f Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Sun, 30 Nov 2025 15:26:53 -0500 Subject: [PATCH 24/31] feature(db): connect messages with db BROKEN --- .../src/androidMain/assets/findmyride.db | Bin 114688 -> 114688 bytes .../example/demo/AndroidMessagesRepository.kt | 120 ++++++++++++++---- .../kotlin/com/example/demo/MainActivity.kt | 22 +++- .../commonMain/kotlin/com/example/demo/App.kt | 18 ++- .../example/demo/feature/main/MainRoute.kt | 17 ++- .../demo/feature/messages/MessagesRoute.kt | 2 +- .../messages/list/MessagesListRoute.kt | 11 +- .../messages/list/MessagesViewModel.kt | 7 +- 8 files changed, 149 insertions(+), 48 deletions(-) diff --git a/app/composeApp/src/androidMain/assets/findmyride.db b/app/composeApp/src/androidMain/assets/findmyride.db index 08fc017a259c4982323501629aaa54e7b379de45..daf2b17c129f34f25daab3a37d762727ca546513 100644 GIT binary patch delta 3116 zcmaJ@OK)366proqoy4>yl)ln2Nt!x|TVGF{+D=R5q)8L?trSWZNFCc_`>NM>+_~3v zT?#iWx~7$osxEjeSR#Q`A&^*f!va{aV1b0hh6+_(@E3|W=h{)?WKu7@xijDQ%{k{g z=d7&-*Vcj$4tVZ8JhVs|ihvM=^e?C03mu?I0b_FnAd_q%R*j>Me( z!9ZWw(?|Q$sdOf(X-S$QS}vc?-c zY;2b=|BU>kR=-=V?~U!x2x8 z`ubO%tv#+2W=PJ;Z6cbsrI)$t%47~vlhn5iCjl1E5>vFcopk`8WD$Qa;A^SW__iZ( zV*CWcjZ)u*cP|r*GfSANW2spLIsriQQjo=ZQP;~vw}@_9%v9=}bux&&NPX8u6OcKv zYn)VBofCsuAX%^0?9+;vBxaB*0aVMZ#)!q*#Nd{tQZ-Jy;tNKLwJlPVV&j$u-tugjH-VRboaoWEiHtd2Vk&Zy?hYBA-HJ-k1il@H|_G zKV~_kX*HRlymcH=Pj^C76{1WkLPA(T1hGY#wN+A*$B;?`s*8d&q$nC33dV+-QF1Ig zNhB#z-_;s3Z(4*^nQpp7C2^ZeES>dSNT{{m;BCBcVj5v4sJCF$1>_B~D3uAHnnHa^ z>h%>23CVSAH|uTDRE0pBMA$j%y~t{$Q~Vf+X~0Ldsw-28b%JgdZBi1I3g@b5jURQ8 zV3c}0?o(O>>OVPwpc<&|h$yz*IxRJ2QKezz2%-*C?+*2lA4j-z2URkQ z0j$zNW{_#2Oxf5Ng2$(v31ocQ*BD z*9E8ELikncJpeJq5b$2+vcgMBKcoqAoTA>9 zXO8>)I_I`a_KKF4IE=^%Kt9jeZ7fWPm6lS)KQn^(@c8T8A`Juj6_1lbqak@o2m2t? z`ZcFJqujU89YTr=KyeoBRu|F)o;X>xb;yy)d6_VRHc-8MiZf~GT&nKPgplC0 z0Y!^&m4B?-+pl$k-^^6J}O#H13 R{73j(VZy4@TlX_o0|4n57K;D? diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidMessagesRepository.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidMessagesRepository.kt index 6d26d58..600f517 100644 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidMessagesRepository.kt +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidMessagesRepository.kt @@ -15,37 +15,103 @@ class AndroidMessagesRepository( * Use logged-in user if available, otherwise fall back to parameter. */ private fun resolveCurrentUserId(paramUserId: Int): Int { - val stored = CurrentUserStore.userId - return stored?.toInt() ?: paramUserId +// val stored = CurrentUserStore.userId +// return stored?.toInt() ?: paramUserId + return 1; } +// override suspend fun getThreadsForUser(userId: Int): List = +// withContext(Dispatchers.IO) { +// listOf( +// MessageThreadUi( +// id = 1, +// senderName = "Debug User", +// initials = "DU", +// lastMessage = "If you see this, repo wiring works!", +// timeAgo = "now", +// unreadCount = 0 +// ) +// ) +// } + + private fun seedFakeMessagesIfEmpty(db: android.database.sqlite.SQLiteDatabase) { + // Check if we already seeded + db.rawQuery("SELECT COUNT(*) FROM MESSAGE_THREAD;", null).use { c -> + if (c.moveToFirst()) { + val count = c.getInt(0) + if (count > 0) return // already has data, no need to seed + } + } + + // ---- THREADS ---- + // All involving user_id = 1 so they'll show when you log in as that user + db.execSQL(""" + INSERT INTO MESSAGE_THREAD (thread_id, user1_id, user2_id) + VALUES + (1, 1, 2), + (2, 1, 3), + (3, 1, 4); + """.trimIndent()) + + // ---- MESSAGES ---- + // Thread 1: Abdul <-> Quincy + db.execSQL(""" + INSERT INTO MESSAGE (thread_id, sender_id, body) VALUES + (1, 1, 'Hey Quincy, are we still on for 5:30 PM?'), + (1, 2, 'Yes! I''ll be there in 10 minutes.'), + (1, 1, 'Perfect, see you soon.'); + """.trimIndent()) + + // Thread 2: Abdul <-> Ame + db.execSQL(""" + INSERT INTO MESSAGE (thread_id, sender_id, body) VALUES + (2, 3, 'Hey Abdul, do you still need a ride tomorrow?'), + (2, 1, 'Yeah! Morning around 9 would be amazing.'), + (2, 3, 'Got you, I''ll swing by then.'); + """.trimIndent()) + + // Thread 3: Abdul <-> Kennan + db.execSQL(""" + INSERT INTO MESSAGE (thread_id, sender_id, body) VALUES + (3, 1, 'Thanks again for the last ride!'), + (3, 4, 'No problem, happy to help.'), + (3, 1, 'I left you a 5-star rating too :)'); + """.trimIndent()) + } + + override suspend fun getThreadsForUser(userId: Int): List = withContext(Dispatchers.IO) { - val db = dbProvider.getReadableDatabase() - val currentUserId = resolveCurrentUserId(userId) + val db = dbProvider.getWritableDatabase() + val currentUserId = 1 // you’re logging in as user_id = 1 + + seedFakeMessagesIfEmpty(db) val sql = """ - SELECT - t.thread_id, - CASE - WHEN t.user1_id = ? THEN u2.username - ELSE u1.username - END AS contact_name, - last_msg.body AS last_message, - last_msg.sent_at AS last_sent_at - FROM MESSAGE_THREAD t - JOIN "USER" u1 ON t.user1_id = u1.user_id - JOIN "USER" u2 ON t.user2_id = u2.user_id - LEFT JOIN MESSAGE last_msg ON last_msg.message_id = ( - SELECT m.message_id - FROM MESSAGE m - WHERE m.thread_id = t.thread_id - ORDER BY m.sent_at DESC, m.message_id DESC - LIMIT 1 - ) - WHERE t.user1_id = ? OR t.user2_id = ? - ORDER BY last_sent_at DESC NULLS LAST, t.thread_id DESC; - """.trimIndent() + SELECT + t.thread_id, + CASE + WHEN t.user1_id = ? THEN u2.username + ELSE u1.username + END AS contact_name, + COALESCE(last_msg.body, 'No messages yet') AS last_message, + last_msg.sent_at AS last_sent_at + FROM MESSAGE_THREAD t + JOIN "USER" u1 ON t.user1_id = u1.user_id + JOIN "USER" u2 ON t.user2_id = u2.user_id + LEFT JOIN MESSAGE last_msg ON last_msg.message_id = ( + SELECT m.message_id + FROM MESSAGE m + WHERE m.thread_id = t.thread_id + ORDER BY m.sent_at DESC, m.message_id DESC + LIMIT 1 + ) + WHERE t.user1_id = ? OR t.user2_id = ? + ORDER BY + (last_sent_at IS NULL), + last_sent_at DESC, + t.thread_id DESC; + """.trimIndent() val args = arrayOf( currentUserId.toString(), @@ -64,7 +130,7 @@ class AndroidMessagesRepository( while (c.moveToNext()) { val name = c.getString(idxName) - val lastMsg = c.getString(idxLastMsg) ?: "No messages yet" + val lastMsg = c.getString(idxLastMsg) val sentAt = c.getString(idxLastSentAt) ?: "" result += MessageThreadUi( @@ -72,7 +138,7 @@ class AndroidMessagesRepository( senderName = name, initials = initialsFromName(name), lastMessage = lastMsg, - timeAgo = sentAt, // keep it simple: show timestamp string + timeAgo = sentAt, unreadCount = 0 ) } diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt index b303bdc..53c04d8 100644 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt @@ -7,9 +7,12 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import com.example.demo.AndroidRideRepository import com.example.demo.App +import com.example.demo.AndroidRideRepository +import com.example.demo.AndroidAuthRepository import com.example.demo.FindMyRideDbProvider +import com.example.demo.feature.profile.data.AndroidProfileRepository +import com.example.demo.feature.messages.data.AndroidMessagesRepository class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -18,10 +21,19 @@ class MainActivity : ComponentActivity() { MaterialTheme { Surface { val context = LocalContext.current - val repo = remember { - AndroidRideRepository(FindMyRideDbProvider(context)) - } - App(rideRepository = repo) + val dbProvider = remember { FindMyRideDbProvider(context) } + + val rideRepo = remember { AndroidRideRepository(dbProvider) } + val profileRepo = remember { AndroidProfileRepository(dbProvider) } + val authRepo = remember { AndroidAuthRepository(dbProvider) } + val messagesRepo = remember { AndroidMessagesRepository(dbProvider) } + + App( + rideRepository = rideRepo, + profileRepository = profileRepo, + authRepository = authRepo, + messagesRepository = messagesRepo + ) } } } diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt index f354121..521371c 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt @@ -2,12 +2,14 @@ package com.example.demo import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* +import com.example.demo.feature.auth.data.AuthRepository import com.example.demo.feature.auth.login.LoginRoute import com.example.demo.feature.auth.signup.SignUpRoute import com.example.demo.feature.auth.forgot.ForgotPasswordRoute import com.example.demo.feature.db.RideRepository import com.example.demo.feature.main.MainRoute -import com.example.demo.feature.profile.ProfileRoute +import com.example.demo.feature.messages.data.MessagesRepository +import com.example.demo.feature.profile.data.ProfileRepository // Top-level screens in your app enum class RootScreen { @@ -18,7 +20,12 @@ enum class RootScreen { } @Composable -fun App(rideRepository: RideRepository) { +fun App( + rideRepository: RideRepository, + profileRepository: ProfileRepository, + authRepository: AuthRepository, + messagesRepository: MessagesRepository +) { var currentScreen by remember { mutableStateOf(RootScreen.Login) } MaterialTheme { @@ -27,7 +34,8 @@ fun App(rideRepository: RideRepository) { RootScreen.Login -> LoginRoute( onNavigateToSignUp = { currentScreen = RootScreen.SignUp }, onNavigateToForgotPassword = { currentScreen = RootScreen.ForgotPassword }, - onLoginSuccess = { currentScreen = RootScreen.Main } + onLoginSuccess = { currentScreen = RootScreen.Main }, + authRepository = authRepository, ) RootScreen.SignUp -> SignUpRoute( @@ -39,7 +47,9 @@ fun App(rideRepository: RideRepository) { ) RootScreen.Main -> MainRoute( - rideRepository = rideRepository + rideRepository = rideRepository, + profileRepository = profileRepository, + messagesRepository = messagesRepository, ) } } diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt index 5ceccc5..6806669 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt @@ -11,6 +11,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import com.example.demo.feature.db.RideRepository import com.example.demo.feature.messages.MessagesRoute +import com.example.demo.feature.messages.data.MessagesRepository +import com.example.demo.feature.profile.data.ProfileRepository import com.example.demo.ui.theme.DrexelBlue import com.example.demo.ui.theme.DrexelGold @@ -18,7 +20,9 @@ enum class MainTab { Home, Rides, Messages, Profile } @Composable fun MainRoute( - rideRepository: RideRepository + rideRepository: RideRepository, + profileRepository: ProfileRepository, + messagesRepository: MessagesRepository, ) { var currentTab by remember { mutableStateOf(MainTab.Home) } // start on profile for now @@ -46,10 +50,17 @@ fun MainRoute( ) } MainTab.Messages -> { - MessagesRoute(modifier = Modifier.padding(padding)) + MessagesRoute( + modifier = Modifier.padding(padding), + repository = messagesRepository, + ) + } MainTab.Profile -> { - ProfileRoute(modifier = Modifier.padding(padding)) + ProfileRoute( + modifier = Modifier.padding(padding), + repository = profileRepository + ) } } } diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/MessagesRoute.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/MessagesRoute.kt index 7c1c121..3d1d1e4 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/MessagesRoute.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/MessagesRoute.kt @@ -28,7 +28,6 @@ fun MessagesRoute( } ) } - MessagesPage.Conversation -> { val thread = activeThread if (thread == null) { @@ -46,3 +45,4 @@ fun MessagesRoute( } } } + diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/list/MessagesListRoute.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/list/MessagesListRoute.kt index 37eb88e..c446f70 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/list/MessagesListRoute.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/list/MessagesListRoute.kt @@ -2,18 +2,21 @@ package com.example.demo.feature.messages.list import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import com.example.demo.feature.messages.data.FakeMessagesRepository import com.example.demo.feature.messages.data.MessagesRepository @Composable fun MessagesListRoute( modifier: Modifier = Modifier, onOpenConversation: (MessageThreadUi) -> Unit, - repository: MessagesRepository = FakeMessagesRepository() + repository: MessagesRepository ) { + // For now, we use userId = 1; AndroidMessagesRepository internally + // will override this with CurrentUserStore.userId when available. val viewModel = remember(repository) { - // userId is still 1 here; Android repo will ignore it and use CurrentUserStore - MessagesViewModel(repository = repository, userId = 1) + MessagesViewModel( + repository = repository, + userId = 1 + ) } val state by viewModel.uiState.collectAsState() diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/list/MessagesViewModel.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/list/MessagesViewModel.kt index 71f9ccc..b1bed78 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/list/MessagesViewModel.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/messages/list/MessagesViewModel.kt @@ -1,7 +1,6 @@ package com.example.demo.feature.messages.list import com.example.demo.feature.messages.data.MessagesRepository -import com.example.demo.feature.messages.data.FakeMessagesRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -10,12 +9,12 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch class MessagesViewModel( - private val repository: MessagesRepository = FakeMessagesRepository(), - private val userId: Int = 1 // fake current user + private val repository: MessagesRepository, + private val userId: Int ) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - private val _uiState = MutableStateFlow(MessagesUiState(isLoading = true)) + private val _uiState = MutableStateFlow(MessagesUiState()) val uiState: StateFlow = _uiState init { From ce2dc5c9d42befe0d13d9e2349743139fc158080 Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Sun, 30 Nov 2025 15:26:58 -0500 Subject: [PATCH 25/31] feature(db): connect messages with db BROKEN --- identifier.sqlite | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 identifier.sqlite diff --git a/identifier.sqlite b/identifier.sqlite new file mode 100644 index 0000000..e69de29 From ee9b5b4d3d05cb37b2986fbe06e80e902e81cfc9 Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Sun, 30 Nov 2025 16:41:11 -0500 Subject: [PATCH 26/31] feature(home_screen): refactor to use images from kotlin --- .../kotlin/com/example/demo/MainActivity.kt | 34 +++++++------- .../feature/rides/AvailableRidesScreen.kt | 21 +++++---- .../example/demo/feature/rides/FieldInputs.kt | 5 ++- .../demo/feature/rides/FindRideScreen.kt | 41 +++++++++-------- .../demo/feature/rides/OfferRideScreen.kt | 45 ++++++++++--------- 5 files changed, 81 insertions(+), 65 deletions(-) diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt index 53c04d8..4181669 100644 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt @@ -13,28 +13,32 @@ import com.example.demo.AndroidAuthRepository import com.example.demo.FindMyRideDbProvider import com.example.demo.feature.profile.data.AndroidProfileRepository import com.example.demo.feature.messages.data.AndroidMessagesRepository +import com.example.demo.feature.rides.AvailableRidesScreen +import com.example.demo.ui.theme.FindRideScreen +import com.example.demo.ui.theme.OfferRideScreen class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { - Surface { - val context = LocalContext.current - val dbProvider = remember { FindMyRideDbProvider(context) } - val rideRepo = remember { AndroidRideRepository(dbProvider) } - val profileRepo = remember { AndroidProfileRepository(dbProvider) } - val authRepo = remember { AndroidAuthRepository(dbProvider) } - val messagesRepo = remember { AndroidMessagesRepository(dbProvider) } - - App( - rideRepository = rideRepo, - profileRepository = profileRepo, - authRepository = authRepo, - messagesRepository = messagesRepo - ) - } +// Surface { +// val context = LocalContext.current +// val dbProvider = remember { FindMyRideDbProvider(context) } +// +// val rideRepo = remember { AndroidRideRepository(dbProvider) } +// val profileRepo = remember { AndroidProfileRepository(dbProvider) } +// val authRepo = remember { AndroidAuthRepository(dbProvider) } +// val messagesRepo = remember { AndroidMessagesRepository(dbProvider) } +// +// App( +// rideRepository = rideRepo, +// profileRepository = profileRepo, +// authRepository = authRepo, +// messagesRepository = messagesRepo +// ) +// } } } } diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt index 016a19e..f6864d2 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt @@ -12,13 +12,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import app.composeapp.generated.resources.Res -import app.composeapp.generated.resources.ic_back_arrow +import androidx.compose.ui.unit.sp import com.example.demo.ui.theme.DrexelBlue import com.example.demo.ui.theme.DrexelGold import com.example.demo.ui.theme.HintGrey import com.example.demo.ui.theme.FieldBackground -import org.jetbrains.compose.resources.painterResource +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.Icon @Composable fun AvailableRidesScreen() { @@ -53,7 +54,7 @@ fun AvailableRidesScreen() { ) { IconButton(onClick = { /* Handle Back */ }) { Icon( - painter = painterResource(Res.drawable.ic_back_arrow), + imageVector = Icons.Filled.ArrowBack, contentDescription = "Back", tint = Color.White ) @@ -65,7 +66,11 @@ fun AvailableRidesScreen() { contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), shape = RoundedCornerShape(20.dp) ) { - Icon(painterResource(Res.drawable.ic_back_arrow), contentDescription = null, modifier = Modifier.size(16.dp)) + Icon( + imageVector = Icons.Filled.FilterList, + contentDescription = "Filter", + modifier = Modifier.size(16.dp) + ) Spacer(modifier = Modifier.width(8.dp)) Text("Filter") } @@ -143,7 +148,7 @@ fun RideCard(ride: RideOption) { Spacer(modifier = Modifier.width(8.dp)) Icon( - painter = painterResource(Res.drawable.ic_back_arrow), + imageVector = Icons.Filled.Star, contentDescription = "Rating", tint = DrexelGold, modifier = Modifier.size(16.dp) @@ -192,7 +197,7 @@ fun RideCard(ride: RideOption) { Spacer(modifier = Modifier.width(8.dp)) Icon( - painter = painterResource(Res.drawable.ic_back_arrow), + imageVector = Icons.Filled.ArrowForward, contentDescription = "to", tint = HintGrey, modifier = Modifier.size(14.dp) @@ -240,4 +245,4 @@ fun SectionBadge( color = textColor ) } -} \ No newline at end of file +} diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FieldInputs.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FieldInputs.kt index 1ed2d3c..baf2f79 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FieldInputs.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FieldInputs.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.example.demo.ui.theme.DrexelBlue @@ -26,7 +27,7 @@ import com.example.demo.ui.theme.HintGrey @Composable fun RideInput( label: String, - icon: Painter, + icon: ImageVector, placeholder: String, modifier: Modifier = Modifier ) { @@ -56,7 +57,7 @@ fun RideInput( leadingIcon = { Icon( - painter = icon, + imageVector = icon, contentDescription = null, tint = HintGrey, modifier = Modifier.size(28.dp) diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt index 95735af..83313d1 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt @@ -6,21 +6,16 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import app.composeapp.generated.resources.Res -import app.composeapp.generated.resources.ic_back_arrow -import app.composeapp.generated.resources.ic_location_ping -import app.composeapp.generated.resources.ic_calendar -import app.composeapp.generated.resources.ic_clock -import app.composeapp.generated.resources.ic_two_people -import app.composeapp.generated.resources.ic_dollar_sign -import org.jetbrains.compose.resources.painterResource import com.example.demo.feature.rides.RideInput +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.Icon @Composable fun FindRideScreen() { @@ -40,9 +35,9 @@ fun FindRideScreen() { .background(DrexelBlue) .padding(24.dp) ) { - IconButton(onClick = {}) { + IconButton(onClick = { /* TODO: handle back nav */ }) { Icon( - painter = painterResource(Res.drawable.ic_back_arrow), + imageVector = Icons.Filled.ArrowBack, contentDescription = "Back", tint = Color.White ) @@ -80,20 +75,28 @@ fun FindRideScreen() { verticalArrangement = Arrangement.spacedBy(16.dp) ) { // Locations - RideInput(label = "Pickup Location", icon = painterResource(Res.drawable.ic_location_ping), placeholder = "30th Street Station") - RideInput(label = "Drop-off Location", icon = painterResource(Res.drawable.ic_location_ping), placeholder = "Cira Green") + RideInput( + label = "Pickup Location", + icon = Icons.Filled.LocationOn, + placeholder = "30th Street Station" + ) + RideInput( + label = "Drop-off Location", + icon = Icons.Filled.LocationOn, + placeholder = "Cira Green" + ) // Date & Time Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { RideInput( label = "Date", - icon = painterResource(Res.drawable.ic_calendar), + icon = Icons.Filled.CalendarMonth, placeholder = "mm/dd/yyyy", modifier = Modifier.weight(1f) ) RideInput( label = "Time", - icon = painterResource(Res.drawable.ic_clock), + icon = Icons.Filled.AccessTime, placeholder = "--:-- --", modifier = Modifier.weight(1f) ) @@ -103,13 +106,13 @@ fun FindRideScreen() { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { RideInput( label = "Seats Needed", - icon = painterResource(Res.drawable.ic_two_people), + icon = Icons.Filled.Group, placeholder = "2", modifier = Modifier.weight(1f) ) RideInput( label = "Max Price", - icon = painterResource(Res.drawable.ic_dollar_sign), + icon = Icons.Filled.AttachMoney, placeholder = "20", modifier = Modifier.weight(1f) ) @@ -119,7 +122,7 @@ fun FindRideScreen() { // Search Button Button( - onClick = {}, + onClick = { /* TODO: trigger search */ }, modifier = Modifier .fillMaxWidth() .height(50.dp), @@ -138,4 +141,4 @@ fun FindRideScreen() { } } } -} \ No newline at end of file +} diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt index 6f50200..50bed4f 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt @@ -6,21 +6,16 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import app.composeapp.generated.resources.Res -import app.composeapp.generated.resources.ic_back_arrow -import app.composeapp.generated.resources.ic_location_ping -import app.composeapp.generated.resources.ic_clock -import app.composeapp.generated.resources.ic_two_people -import app.composeapp.generated.resources.ic_dollar_sign -import app.composeapp.generated.resources.ic_vehicle -import org.jetbrains.compose.resources.painterResource import com.example.demo.feature.rides.RideInput +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.Icon @Composable fun OfferRideScreen() { @@ -40,9 +35,9 @@ fun OfferRideScreen() { .background(DrexelBlue) .padding(24.dp) ) { - IconButton(onClick = {}) { + IconButton(onClick = { /* TODO: back nav */ }) { Icon( - painter = painterResource(Res.drawable.ic_back_arrow), + imageVector = Icons.Filled.ArrowBack, contentDescription = "Back", tint = Color.White ) @@ -80,27 +75,35 @@ fun OfferRideScreen() { verticalArrangement = Arrangement.spacedBy(16.dp) ) { // Locations - RideInput(label = "Origin Location", icon = painterResource(Res.drawable.ic_location_ping), placeholder = "University Crossings") - RideInput(label = "Destination", icon = painterResource(Res.drawable.ic_location_ping), placeholder = "Korman Center") + RideInput( + label = "Origin Location", + icon = Icons.Filled.LocationOn, + placeholder = "University Crossings" + ) + RideInput( + label = "Destination", + icon = Icons.Filled.LocationOn, + placeholder = "Korman Center" + ) // Departure Time RideInput( label = "Departure Time", - icon = painterResource(Res.drawable.ic_clock), + icon = Icons.Filled.AccessTime, placeholder = "mm/dd/yyyy --:-- --", ) // Vehicle Selection RideInput( label = "Choose Vehicle", - icon = painterResource(Res.drawable.ic_vehicle), + icon = Icons.Filled.DirectionsCar, placeholder = "Tesla Model Y (Blue)", ) // Available Seats RideInput( label = "Available Seats", - icon = painterResource(Res.drawable.ic_two_people), + icon = Icons.Filled.Group, placeholder = "2", ) @@ -108,13 +111,13 @@ fun OfferRideScreen() { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { RideInput( label = "Base Price", - icon = painterResource(Res.drawable.ic_dollar_sign), + icon = Icons.Filled.AttachMoney, placeholder = "5.00", modifier = Modifier.weight(1f) ) RideInput( label = "Per Mile", - icon = painterResource(Res.drawable.ic_dollar_sign), + icon = Icons.Filled.AttachMoney, placeholder = "0.50", modifier = Modifier.weight(1f) ) @@ -122,9 +125,9 @@ fun OfferRideScreen() { Spacer(modifier = Modifier.height(4.dp)) - // Search Button + // Publish Button Button( - onClick = {}, + onClick = { /* TODO: publish offer */ }, modifier = Modifier .fillMaxWidth() .height(50.dp), @@ -143,4 +146,4 @@ fun OfferRideScreen() { } } } -} \ No newline at end of file +} From ea2c541179f084bb8bf98b462a2a9c771d3a1fb9 Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Sun, 30 Nov 2025 17:30:25 -0500 Subject: [PATCH 27/31] feature(my_ride): create my ride screen to show history of rides --- .../kotlin/com/example/demo/MainActivity.kt | 32 +- .../example/demo/feature/main/HomeScreen.kt | 238 ++++++++++++ .../example/demo/feature/main/MainRoute.kt | 12 +- .../example/demo/feature/main/MyRideScreen.kt | 352 ++++++++++++++++++ 4 files changed, 612 insertions(+), 22 deletions(-) create mode 100644 app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/HomeScreen.kt create mode 100644 app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MyRideScreen.kt diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt index 4181669..f2d0939 100644 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt @@ -22,23 +22,23 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { MaterialTheme { + AvailableRidesScreen() + Surface { + val context = LocalContext.current + val dbProvider = remember { FindMyRideDbProvider(context) } -// Surface { -// val context = LocalContext.current -// val dbProvider = remember { FindMyRideDbProvider(context) } -// -// val rideRepo = remember { AndroidRideRepository(dbProvider) } -// val profileRepo = remember { AndroidProfileRepository(dbProvider) } -// val authRepo = remember { AndroidAuthRepository(dbProvider) } -// val messagesRepo = remember { AndroidMessagesRepository(dbProvider) } -// -// App( -// rideRepository = rideRepo, -// profileRepository = profileRepo, -// authRepository = authRepo, -// messagesRepository = messagesRepo -// ) -// } + val rideRepo = remember { AndroidRideRepository(dbProvider) } + val profileRepo = remember { AndroidProfileRepository(dbProvider) } + val authRepo = remember { AndroidAuthRepository(dbProvider) } + val messagesRepo = remember { AndroidMessagesRepository(dbProvider) } + + App( + rideRepository = rideRepo, + profileRepository = profileRepo, + authRepository = authRepo, + messagesRepository = messagesRepo + ) + } } } } diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/HomeScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/HomeScreen.kt new file mode 100644 index 0000000..d7b996d --- /dev/null +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/HomeScreen.kt @@ -0,0 +1,238 @@ +package com.example.demo.feature.main + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DirectionsCar +import androidx.compose.material.icons.filled.People +import androidx.compose.material.icons.filled.Settings +import androidx.compose.ui.graphics.vector.ImageVector +import com.example.demo.ui.theme.DrexelBlue +import com.example.demo.ui.theme.DrexelGold +import com.example.demo.ui.theme.FieldBackground +import com.example.demo.ui.theme.HintGrey + + +@Composable +fun HomeScreen( + modifier: Modifier = Modifier, + onFindRideClick: () -> Unit = {}, + onOfferRideClick: () -> Unit = {} +) { + Column( + modifier = modifier + .fillMaxSize() + .background(FieldBackground) + ) { + // ---------- HEADER ---------- + Box( + modifier = Modifier + .fillMaxWidth() + .background(DrexelBlue) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 24.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = "Welcome back!", + style = MaterialTheme.typography.headlineSmall, + color = Color.White, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Where would you like to go?", + style = MaterialTheme.typography.bodyMedium, + color = Color.White.copy(alpha = 0.8f) + ) + } + + IconButton(onClick = { /* settings later */ }) { + Icon( + imageVector = Icons.Filled.Settings, + contentDescription = "Settings", + tint = Color.White + ) + } + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // ---------- MAIN CARDS ---------- + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .weight(1f, fill = true), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Find a Ride card + HomeActionCard( + title = "Find a Ride", + subtitle = "Search for available rides to your destination", + iconBgColor = Color(0xFFE3F2FD), + iconTint = DrexelBlue, + icon = Icons.Filled.People, + buttonText = "Get started →", + onClick = onFindRideClick + ) + + // Offer a Ride card + HomeActionCard( + title = "Offer a Ride", + subtitle = "Share your trip and earn money", + iconBgColor = Color(0xFFFFF4CC), + iconTint = DrexelGold, + icon = Icons.Filled.DirectionsCar, + buttonText = "Get started →", + onClick = onOfferRideClick + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Stats row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + StatCard( + label = "Rides", + value = "12", + modifier = Modifier.weight(1f) + ) + StatCard( + label = "Rating", + value = "4.8★", + modifier = Modifier.weight(1f) + ) + StatCard( + label = "Saved", + value = "$142", + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@Composable +private fun HomeActionCard( + title: String, + subtitle: String, + iconBgColor: Color, + iconTint: Color, + icon: ImageVector, + buttonText: String, + onClick: () -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Column( + modifier = Modifier + .padding(horizontal = 20.dp, vertical = 16.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = DrexelBlue + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = HintGrey + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(44.dp) + .clip(CircleShape) + .background(iconBgColor), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = iconTint + ) + } + + TextButton(onClick = onClick) { + Text( + text = buttonText, + color = DrexelBlue, + fontWeight = FontWeight.SemiBold + ) + } + } + } + } +} + +@Composable +private fun StatCard( + label: String, + value: String, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier, + shape = RoundedCornerShape(18.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier + .padding(vertical = 12.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = value, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = DrexelBlue + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = label, + style = MaterialTheme.typography.bodySmall, + color = HintGrey + ) + } + } +} diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt index 6806669..2951f18 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt @@ -13,6 +13,7 @@ import com.example.demo.feature.db.RideRepository import com.example.demo.feature.messages.MessagesRoute import com.example.demo.feature.messages.data.MessagesRepository import com.example.demo.feature.profile.data.ProfileRepository +import com.example.demo.feature.rides.MyRidesScreen import com.example.demo.ui.theme.DrexelBlue import com.example.demo.ui.theme.DrexelGold @@ -37,15 +38,14 @@ fun MainRoute( ) { padding -> when (currentTab) { MainTab.Home -> { - // TODO: replace with real HomeRoute - Text( - text = "Home screen placeholder", - modifier = Modifier.padding(padding) + HomeScreen( + modifier = Modifier.padding(padding), + onFindRideClick = { currentTab = MainTab.Rides }, + onOfferRideClick = { currentTab = MainTab.Rides } ) } MainTab.Rides -> { - Text( - text = "My Rides placeholder", + MyRidesScreen( modifier = Modifier.padding(padding) ) } diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MyRideScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MyRideScreen.kt new file mode 100644 index 0000000..8e7733f --- /dev/null +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MyRideScreen.kt @@ -0,0 +1,352 @@ +package com.example.demo.feature.rides + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CalendarToday +import androidx.compose.material.icons.filled.LocationOn +import com.example.demo.ui.theme.DrexelBlue +import com.example.demo.ui.theme.FieldBackground +import com.example.demo.ui.theme.HintGrey + +private enum class MyRidesTab { Upcoming, Completed } + +data class RideHistoryItem( + val id: Int, + val status: String, + val role: String, + val driverName: String, + val pickup: String, + val dropoff: String, + val date: String, + val time: String, + val price: String +) + +@Composable +fun MyRidesScreen( + modifier: Modifier = Modifier +) { + // fake data for now + val upcomingRides = listOf( + RideHistoryItem( + id = 1, + status = "Confirmed", + role = "Passenger", + driverName = "Abdul B.", + pickup = "30th Street Station", + dropoff = "Cira Green", + date = "Nov 11, 2025", + time = "5:30 PM", + price = "$11.71" + ) + ) + + val completedRides = listOf( + RideHistoryItem( + id = 2, + status = "Passenger", // only pill we show + role = "", // leave empty so no second pill + driverName = "Sarah M.", + pickup = "University Crossings", + dropoff = "Downtown Philadelphia", + date = "Nov 5, 2025", + time = "8:00 AM", + price = "$15.5" + ), + RideHistoryItem( + id = 3, + status = "Driver", + role = "", + driverName = "You", // or whoever + pickup = "West Philadelphia", + dropoff = "King of Prussia", + date = "Nov 1, 2025", + time = "6:30 PM", + price = "$28" + ), + RideHistoryItem( + id = 4, + status = "Passenger", + role = "", + driverName = "Michael T.", + pickup = "Temple University", + dropoff = "Chestnut Hill", + date = "Oct 28, 2025", + time = "3:15 PM", + price = "$12.75" + ) + ) + + var currentTab by remember { mutableStateOf(MyRidesTab.Upcoming) } + + Column( + modifier = modifier + .fillMaxSize() + .background(FieldBackground) + ) { + // ------- HEADER -------- + Box( + modifier = Modifier + .fillMaxWidth() + .background(DrexelBlue) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 24.dp) + ) { + Text( + text = "My Rides", + style = MaterialTheme.typography.headlineSmall, + color = Color.White, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Your ride history & upcoming trips", + style = MaterialTheme.typography.bodyMedium, + color = Color.White.copy(alpha = 0.8f) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // segmented control + Surface( + shape = RoundedCornerShape(24.dp), + color = Color.White, + tonalElevation = 4.dp, + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + ) { + Row( + modifier = Modifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically + ) { + SegmentedTab( + text = "Upcoming", + selected = currentTab == MyRidesTab.Upcoming, + modifier = Modifier.weight(1f) + ) { currentTab = MyRidesTab.Upcoming } + + SegmentedTab( + text = "Completed", + selected = currentTab == MyRidesTab.Completed, + modifier = Modifier.weight(1f) + ) { currentTab = MyRidesTab.Completed } + } + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // ------- LIST CONTENT ------- + val ridesToShow = + if (currentTab == MyRidesTab.Upcoming) upcomingRides else completedRides + + LazyColumn( + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(ridesToShow, key = { it.id }) { ride -> + RideHistoryCard(ride) + } + + if (ridesToShow.isEmpty()) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = "No rides in this section yet.", + color = HintGrey + ) + } + } + } + } + } +} + +@Composable +private fun SegmentedTab( + text: String, + selected: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + val bg = if (selected) Color.White else Color.Transparent + val textColor = if (selected) DrexelBlue else HintGrey + + Box( + modifier = modifier + .fillMaxHeight() + ) { + TextButton( + onClick = onClick, + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues(0.dp) + ) { + Surface( + shape = RoundedCornerShape(24.dp), + color = if (selected) Color(0xFFEFF3FF) else Color.Transparent + ) { + Box( + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 6.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + fontSize = 14.sp, + fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal, + color = textColor + ) + } + } + } + } +} + +@Composable +private fun RideHistoryCard(ride: RideHistoryItem) { + Card( + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier + .padding(horizontal = 20.dp, vertical = 16.dp) + ) { + // status row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + // always show the first pill + PillChip( + text = ride.status, + bg = Color(0xFFE3F8EA), + textColor = Color(0xFF2C7A43) + ) + + // only show second pill when role is non-blank + if (ride.role.isNotBlank()) { + PillChip( + text = ride.role, + bg = Color(0xFFEAF0FF), + textColor = DrexelBlue + ) + } + } + Text( + text = ride.price, + fontWeight = FontWeight.Bold, + color = DrexelBlue + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Driver: ${ride.driverName}", + style = MaterialTheme.typography.bodyMedium, + color = HintGrey + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // locations + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Filled.LocationOn, + contentDescription = null, + tint = HintGrey, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = ride.pickup, + style = MaterialTheme.typography.bodyMedium, + color = HintGrey + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Filled.LocationOn, + contentDescription = null, + tint = HintGrey, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = ride.dropoff, + style = MaterialTheme.typography.bodyMedium, + color = HintGrey + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // date + time + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Filled.CalendarToday, + contentDescription = null, + tint = HintGrey, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "${ride.date} ${ride.time}", + style = MaterialTheme.typography.bodyMedium, + color = HintGrey + ) + } + } + } +} + +@Composable +private fun PillChip( + text: String, + bg: Color, + textColor: Color +) { + Surface( + color = bg, + shape = RoundedCornerShape(50) + ) { + Text( + text = text, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + color = textColor, + modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp) + ) + } +} From 427467db16027d82c23cdafd3cfcddbd62081ba0 Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Sun, 30 Nov 2025 22:07:58 -0500 Subject: [PATCH 28/31] docs: Finished README.md --- README.md | 307 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 296 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 95e107e..bf5454b 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,279 @@ -# Find-My-Ride -[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org) +# Find My Ride – Kotlin Multiplatform Ride-Sharing App +### Drexel University – CS 461 Final Project +### Built by: +- Mustafa Bookwala +- Samii Shabuse +- Kennan Lu -# Coding Standards +--- +## Overview -## Commit Messages +**Find My Ride** is a full-stack **Kotlin Multiplatform (KMP)** mobile application that simulates a Drexel-focused ride-sharing service. +The app allows students to: -Following the committing convention of [Conventional Commits](conventionalcommits.org/en/) +- Create an account & log in +- View their profile +- Browse available ride offers +- Publish a ride offer as a driver +- Send messages to other users +- Store all data in a pre-loaded SQLite database -# Authors -- Mustafa Bookwala -- Samii Shabuse -- Kennan Lu +The purpose of the project is to demonstrate **end-to-end database integration**, clean architecture, multiplatform UI, and CRUD operations learned in CS461. + +--- + +## Tech Stack + +### **Frontend / App UI** +- **Kotlin Multiplatform (KMP)** +- **JetBrains Compose Multiplatform** +- **Material3 Components** +- **Navigation built manually via sealed classes** + +### **Database Layer** +- **SQLite** (preloaded DB file in `assets/findmyride.db`) +- **Custom repository layer** for all DB tables +- **Android SQLiteOpenHelper** for managing the database (Android-only) +- **Raw SQL queries** (no ORM) for maximum clarity + +### **Platforms Supported** +- **Android** (Primary target) +- **Compose Desktop** (Compilation supported, no DB) +- KMP structure prepared for iOS/JS, but **DB only implemented on Android** for this assignment. + +--- + +## Project Structure +```SQL +composeApp/ +├── src/ +│ ├── commonMain/ +│ │ ├── feature/ +│ │ │ ├── auth/ # Login, Registration +│ │ │ ├── profile/ # User profile screen +│ │ │ ├── rides/ # Find Ride & Offer Ride +│ │ │ ├── messages/ # Messaging system +│ │ │ └── db/ # Repository interfaces & shared data models +│ │ ├── App.kt # Root-level navigation deciding Login/Main +│ │ └── MainRoute.kt # In-app navigation (Dashboard → Screens) +│ │ +│ ├── androidMain/ +│ │ ├── db/ +│ │ │ ├── FindMyRideDbProvider.kt # Loads SQLite DB from assets +│ │ │ ├── AndroidAuthRepository.kt # USER table +│ │ │ ├── AndroidProfileRepository.kt # USER + VEHICLE +│ │ │ ├── AndroidMessagesRepository.kt # MESSAGE table +│ │ │ └── AndroidRideRepository.kt # RIDE_OFFER + RIDE_REQUEST +│ │ └── MainActivity.kt # Android entry point +│ │ +│ └── iosMain/ # (Not used) +│ +└── assets/ +└── findmyride.db # Preloaded SQLite database +``` + + +--- + +## Database Schema Overview + +The app uses a **pre-populated SQLite database** stored in `assets/findmyride.db`. +This database contains all tables required for the ride-sharing scenario: + +### **USER** +| Column | Type | Description | +|--------|------|-------------| +| user_id | INTEGER PK | Unique user ID | +| email | TEXT | Login credential | +| password | TEXT | User password | +| display_name | TEXT | Display name | +| phone | TEXT | User contact number | + +### **VEHICLE** +| Column | Type | +|--------|------| +| vehicle_id | INTEGER PK | +| owner_user_id | INTEGER FK → USER | +| make | TEXT | +| model | TEXT | +| color | TEXT | + +### **LOCATION** +Preset Drexel locations: +- University Crossings +- Korman Center +- Main Building +- etc. + +### **RIDE_OFFER** +Driver-created ride offers +(Read by AvailableRidesScreen, written by AvailableOfferScreen) + +| Column | Type | +|--------|------| +| offer_id | INTEGER PK | +| driver_id | INTEGER FK → USER | +| vehicle_id | INTEGER FK → VEHICLE | +| original_location_id | INTEGER FK → LOCATION | +| dest_location_id | INTEGER FK → LOCATION | +| depart_at | TEXT | +| seats_available | INTEGER | +| price_base | REAL | +| price_per_mile | REAL | +| status | TEXT | + +### **RIDE_REQUEST** +Stores ride requests created by riders +(Used by repository, not fully exposed in UI) + +### **MESSAGE** +Messaging between users +| Column | Type | +|--------|------| +| message_id | INTEGER PK | +| sender_id | INTEGER FK → USER | +| receiver_id | INTEGER | +| content | TEXT | +| timestamp | TEXT | + +--- + +## Database Architecture & Access Layer + +This project uses a clean Repository Pattern to keep all SQLite logic isolated to the Android layer while allowing the UI layer (commonMain) to stay fully multiplatform. + +1. FindMyRideDbProvider (Android-only) + +- Loads the SQLite database file (findmyride.db) from the assets folder. +- Copies it into Android internal storage on first launch. +- Exposes functions for obtaining readable and writable database instances. +- Ensures all repositories use the same initialized database. + +Located at: androidMain/db/FindMyRideDbProvider.kt + + +2. Repository Interfaces (commonMain) + +Each feature defines an interface that represents its database API. These interfaces are platform-agnostic. + +Example: RideRepository defines functions like: +- getOpenRideOffers() +- createRideOffer(...) + +The UI talks ONLY to these interfaces, never SQLite directly. + + +3. Android Implementations (androidMain) + +Each repository interface has a real implementation in androidMain that uses: +- SQLiteDatabase +- rawQuery() +- insert() +- update() +- delete() + +Examples: +- AndroidRideRepository reads/writes RIDE_OFFER and RIDE_REQUEST +- AndroidAuthRepository reads USER for login +- AndroidProfileRepository reads USER + VEHICLE +- AndroidMessagesRepository reads/writes MESSAGE + +This keeps SQL isolated to Android while UI stays pure KMP. -## Database Setup +4. UI Layer (commonMain) Consumes Repositories -1. Install SQLite if you don’t have it (`sqlite3 --version` to check). +The UI never touches SQLite. + +Examples: +- Reading from DB: rideRepository.getOpenRideOffers() +- Writing to DB: rideRepository.createRideOffer(...) + +This separation ensures portability and clean architecture. + + +------------------------------------------------------------- + +## Feature-by-Feature Explanation + +1. Login Screen +- Reads from USER through AuthRepository. +- Validates email + password. +- Stores currentUserId inside MainRoute. +- On success -> navigates to Dashboard. + + +2. Dashboard + Acts as the main navigation hub for: +- Profile +- Find Ride +- Offer Ride +- Messages + +No DB calls here. + + +3. AvailableRidesScreen — Database READ +- Uses rideRepository.getOpenRideOffers(). +- Shows list of all open rides from RIDE_OFFER table. +- Displays origin, destination, seats, price, driver info. +- Splits results into “Best Matches” and “Other Matches”. + +Backend: AndroidRideRepository using SELECT queries. + + +4. AvailableOfferScreen — Database WRITE + This screen lets a driver publish a ride. + +When user taps Publish: +- createRideOffer() is called with driverId, vehicleId, locations, pricing, etc. +- This performs INSERT INTO RIDE_OFFER. +- App returns to Dashboard afterward. + + +5. Profile Screen — Database READ + Reads from USER and VEHICLE: +- display_name +- email +- phone +- car make/model/color + +Used to show account info. + + +6. Messages Screen — Database READ & WRITE +- Reads conversations via SELECT on MESSAGE table. +- Displays messages in a thread-style list. +- Sends messages using INSERT INTO MESSAGE. +- Powered by MessagesRepository and AndroidMessagesRepository. + + +------------------------------------------------------------- + +## How To Run the Project + +Requirements: +- Android Studio (Ladybug / Koala / modern KMP version) +- Kotlin Multiplatform plugin enabled +- Android SDK 34 or higher + +Steps: +1. Open the project in Android Studio. +2. Wait for Gradle sync to complete. +3. Select run configuration: composeApp:androidApp +4. Run on emulator or Android device. +5. App starts and automatically copies SQLite database from assets. + +No server required. +No external API required. +Database is bundled inside the APK. + +------------------------------------------------------------- + +## How To Create A New Database + +1. Install SQLite if you don’t have it (`sqlite3 --version` to check). - Using Version: 3.50.4 2025-07-30 19:33:53 4d8adfb30e03f9cf27f800a2c1ba3c48fb4ca1b08b0f5ed59a4d5ecbf45e20a3 (64-bit) 2. In the root project folder, run: ```bash @@ -27,3 +284,31 @@ sqlite3 findmyride.db < database/schema.sql sqlite3 findmyride.db sqlite> .tables ``` + +------------------------------------------------------------- + +## Key Implementation Notes + +- UI state uses Jetpack Compose’s remember { mutableStateOf() }. +- Navigation uses sealed classes (RootScreen, HomePage). +- UI + Repository Interfaces live in commonMain. +- SQLite logic lives exclusively in androidMain. +- Database access uses raw SQL for transparency (no ORM). +- Database lifecycle & copying handled by FindMyRideDbProvider. + + +------------------------------------------------------------- + +## Future Improvements + +- Add ride filtering by time, price, seats. +- Add vehicle picker tied to VEHICLE table. +- Add a full chat UI per ride. +- Support for iOS using SQLDelight or KMP-SQLite. +- Add push-style notifications for new rides. +- Implement ride request matching algorithm. + + +------------------------------------------------------------- + + From a5c5378000e3911c31e061dda4e95613a29fb244 Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Sun, 30 Nov 2025 22:08:16 -0500 Subject: [PATCH 29/31] database: update database --- .../src/androidMain/assets/findmyride.db | Bin 114688 -> 122880 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/composeApp/src/androidMain/assets/findmyride.db b/app/composeApp/src/androidMain/assets/findmyride.db index daf2b17c129f34f25daab3a37d762727ca546513..7019dce45b9a7a55334a0ce7bba75b6794cc99e4 100644 GIT binary patch delta 4590 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 Date: Sun, 30 Nov 2025 22:46:51 -0500 Subject: [PATCH 30/31] push --- .idea/deploymentTargetSelector.xml | 3 + .../com/example/demo/AndroidRideRepository.kt | 30 ++ .../kotlin/com/example/demo/MainActivity.kt | 2 - .../commonMain/kotlin/com/example/demo/App.kt | 1 + .../example/demo/feature/db/RideRepository.kt | 11 + .../example/demo/feature/main/MainRoute.kt | 45 ++- .../feature/rides/AvailableOffersScreen.kt | 350 ++++++++++++++++++ .../feature/rides/AvailableRidesScreen.kt | 240 ++++++------ .../demo/feature/rides/OfferRideScreen.kt | 116 +++--- 9 files changed, 597 insertions(+), 201 deletions(-) diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 09a2ea2..87b2916 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -33,6 +33,9 @@ + + \ No newline at end of file diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideRepository.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideRepository.kt index f3b3e22..335ba12 100644 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideRepository.kt +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideRepository.kt @@ -84,4 +84,34 @@ class AndroidRideRepository( db.insert("RIDE_REQUEST", null, values) } } + + override suspend fun createRideOffer( + driverId: Long, + vehicleId: Long, + originalLocationId: Long, + destLocationId: Long, + departAt: String, + seatsAvailable: Int, + priceBase: Double, + pricePerMile: Double + ) { + withContext(Dispatchers.IO) { + val db = dbProvider.getWritableDatabase() + + val values = ContentValues().apply { + put("driver_id", driverId) + put("vehicle_id", vehicleId) + put("original_location_id", originalLocationId) + put("dest_location_id", destLocationId) + put("depart_at", departAt) + put("seats_available", seatsAvailable) + put("price_base", priceBase) + put("price_per_mile", pricePerMile) + put("status", "open") + } + + db.insert("RIDE_OFFER", null, values) + } + } + } diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt index f2d0939..628becb 100644 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt @@ -15,14 +15,12 @@ import com.example.demo.feature.profile.data.AndroidProfileRepository import com.example.demo.feature.messages.data.AndroidMessagesRepository import com.example.demo.feature.rides.AvailableRidesScreen import com.example.demo.ui.theme.FindRideScreen -import com.example.demo.ui.theme.OfferRideScreen class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { - AvailableRidesScreen() Surface { val context = LocalContext.current val dbProvider = remember { FindMyRideDbProvider(context) } diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt index 521371c..129cb15 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt @@ -50,6 +50,7 @@ fun App( rideRepository = rideRepository, profileRepository = profileRepository, messagesRepository = messagesRepository, + ) } } diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/db/RideRepository.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/db/RideRepository.kt index 750c60d..d61f199 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/db/RideRepository.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/db/RideRepository.kt @@ -21,4 +21,15 @@ interface RideRepository { latestPickup: String?, seatsNeeded: Int ) + + suspend fun createRideOffer( + driverId: Long, + vehicleId: Long, + originalLocationId: Long, + destLocationId: Long, + departAt: String, + seatsAvailable: Int, + priceBase: Double, + pricePerMile: Double + ) } diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt index 2951f18..1b85323 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/main/MainRoute.kt @@ -13,11 +13,14 @@ import com.example.demo.feature.db.RideRepository import com.example.demo.feature.messages.MessagesRoute import com.example.demo.feature.messages.data.MessagesRepository import com.example.demo.feature.profile.data.ProfileRepository +import com.example.demo.feature.rides.AvailableOfferScreen +import com.example.demo.feature.rides.AvailableRidesScreen import com.example.demo.feature.rides.MyRidesScreen import com.example.demo.ui.theme.DrexelBlue import com.example.demo.ui.theme.DrexelGold enum class MainTab { Home, Rides, Messages, Profile } +private enum class HomePage { Dashboard, AvailableRides, OfferRide } @Composable fun MainRoute( @@ -25,24 +28,50 @@ fun MainRoute( profileRepository: ProfileRepository, messagesRepository: MessagesRepository, ) { - var currentTab by remember { mutableStateOf(MainTab.Home) } // start on profile for now + var currentTab by remember { mutableStateOf(MainTab.Home) } + var homePage by remember { mutableStateOf(HomePage.Dashboard) } Scaffold( bottomBar = { MainBottomNav( currentTab = currentTab, - onTabSelected = { currentTab = it } + onTabSelected = { tab -> + currentTab = tab + if (tab == MainTab.Home) { + homePage = HomePage.Dashboard // reset when returning to Home tab + } + } ) }, containerColor = Color(0xFFF5F5F7) ) { padding -> when (currentTab) { MainTab.Home -> { - HomeScreen( - modifier = Modifier.padding(padding), - onFindRideClick = { currentTab = MainTab.Rides }, - onOfferRideClick = { currentTab = MainTab.Rides } - ) + when (homePage) { + HomePage.Dashboard -> { + HomeScreen( + modifier = Modifier.padding(padding), + onFindRideClick = { homePage = HomePage.AvailableRides }, + onOfferRideClick = { homePage = HomePage.OfferRide }, + ) + } + HomePage.AvailableRides -> { + AvailableRidesScreen( + modifier = Modifier.padding(padding), + onBack = { homePage = HomePage.Dashboard }, + rideRepository = rideRepository + ) + } + HomePage.OfferRide -> { + AvailableOfferScreen( + modifier = Modifier.padding(padding), + onBack = { homePage = HomePage.Dashboard }, + onPublish = { + homePage = HomePage.Dashboard + } + ) + } + } } MainTab.Rides -> { MyRidesScreen( @@ -54,7 +83,6 @@ fun MainRoute( modifier = Modifier.padding(padding), repository = messagesRepository, ) - } MainTab.Profile -> { ProfileRoute( @@ -66,6 +94,7 @@ fun MainRoute( } } + @Composable private fun MainBottomNav( currentTab: MainTab, diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableOffersScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableOffersScreen.kt index fe994e3..58d8143 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableOffersScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableOffersScreen.kt @@ -1,2 +1,352 @@ package com.example.demo.feature.rides +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.demo.ui.theme.DrexelBlue +import com.example.demo.ui.theme.DrexelGold +import com.example.demo.ui.theme.FieldBackground +import com.example.demo.ui.theme.HintGrey + +@Composable +fun AvailableOfferScreen( + modifier: Modifier = Modifier, + onBack: () -> Unit = {}, + onPublish: () -> Unit = {} +) { + var origin by remember { mutableStateOf("University Crossings") } + var destination by remember { mutableStateOf("Korman Center") } + var departure by remember { mutableStateOf("") } + + var selectedVehicle by remember { mutableStateOf("Tesla Model Y (Blue)") } + val vehicleOptions = listOf( + "Tesla Model Y (Blue)", + "Honda Civic (Black)", + "Toyota Camry (Silver)" + ) + + var selectedSeats by remember { mutableStateOf("2 Seats") } + val seatOptions = listOf("1 Seat", "2 Seats", "3 Seats", "4 Seats") + + var basePrice by remember { mutableStateOf("5.00") } + var perMilePrice by remember { mutableStateOf("0.50") } + + var scrollState = rememberScrollState() + + Column( + modifier = modifier + .fillMaxSize() + .background(FieldBackground) + .verticalScroll(scrollState) + ) { + // ---------- HEADER ---------- + Box( + modifier = Modifier + .fillMaxWidth() + .background(DrexelBlue) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 24.dp) + ) { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back", + tint = Color.White + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Offer a Ride", + style = MaterialTheme.typography.headlineSmall, + color = Color.White, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "Share your journey details", + style = MaterialTheme.typography.bodyMedium, + color = Color.White.copy(alpha = 0.8f) + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // ---------- MAIN CARD ---------- + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 6.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Origin + LabeledIconField( + label = "Origin Location", + value = origin, + onValueChange = { origin = it }, + icon = Icons.Default.Place, + placeholder = "University Crossings" + ) + + // Destination + LabeledIconField( + label = "Destination", + value = destination, + onValueChange = { destination = it }, + icon = Icons.Default.Place, + placeholder = "Korman Center" + ) + + // Departure Time + LabeledIconField( + label = "Departure Time", + value = departure, + onValueChange = { departure = it }, + icon = Icons.Default.AccessTime, + placeholder = "mm/dd/yyyy --:-- --" + ) + + // Vehicle dropdown + LabeledDropdownField( + label = "Choose Vehicle", + value = selectedVehicle, + onValueChange = { selectedVehicle = it }, + options = vehicleOptions, + leadingIcon = Icons.Default.DirectionsCar + ) + + // Seats dropdown + LabeledDropdownField( + label = "Available Seats", + value = selectedSeats, + onValueChange = { selectedSeats = it }, + options = seatOptions, + leadingIcon = Icons.Default.People + ) + + // Prices row + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.fillMaxWidth() + ) { + LabeledPriceField( + label = "Base Price", + value = basePrice, + onValueChange = { basePrice = it }, + modifier = Modifier.weight(1f) + ) + + LabeledPriceField( + label = "Per Mile", + value = perMilePrice, + onValueChange = { perMilePrice = it }, + modifier = Modifier.weight(1f) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Publish button + Button( + onClick = onPublish, + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + colors = ButtonDefaults.buttonColors( + containerColor = DrexelGold, + contentColor = DrexelBlue + ), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Publish Offer", + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +/* ---------- Reusable components used on the screen ---------- */ + +@Composable +private fun FieldLabel(text: String) { + Text( + text = text, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + color = DrexelBlue, + modifier = Modifier.padding(bottom = 4.dp) + ) +} + +@Composable +private fun LabeledIconField( + label: String, + value: String, + onValueChange: (String) -> Unit, + icon: androidx.compose.ui.graphics.vector.ImageVector, + placeholder: String +) { + Column { + FieldLabel(label) + + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { + Icon( + imageVector = icon, + contentDescription = null, + tint = HintGrey + ) + }, + placeholder = { Text(text = placeholder, color = HintGrey) }, + shape = RoundedCornerShape(10.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = Color(0xFFF5F6F8), + unfocusedContainerColor = Color(0xFFF5F6F8), + disabledContainerColor = Color(0xFFF5F6F8), + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + focusedTextColor = DrexelBlue, + unfocusedTextColor = DrexelBlue + ), + singleLine = true + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun LabeledDropdownField( + label: String, + value: String, + onValueChange: (String) -> Unit, + options: List, + leadingIcon: androidx.compose.ui.graphics.vector.ImageVector +) { + var expanded by remember { mutableStateOf(false) } + + Column { + FieldLabel(label) + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded } + ) { + OutlinedTextField( + value = value, + onValueChange = {}, + readOnly = true, + modifier = Modifier + .menuAnchor() + .fillMaxWidth(), + leadingIcon = { + Icon( + imageVector = leadingIcon, + contentDescription = null, + tint = HintGrey + ) + }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) + }, + shape = RoundedCornerShape(10.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = Color(0xFFF5F6F8), + unfocusedContainerColor = Color(0xFFF5F6F8), + disabledContainerColor = Color(0xFFF5F6F8), + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + focusedTextColor = DrexelBlue, + unfocusedTextColor = DrexelBlue + ), + singleLine = true + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + options.forEach { option -> + DropdownMenuItem( + text = { Text(option) }, + onClick = { + onValueChange(option) + expanded = false + } + ) + } + } + } + } +} + +@Composable +private fun LabeledPriceField( + label: String, + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + FieldLabel(label) + + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { + Text( + text = "$", + color = HintGrey, + fontWeight = FontWeight.SemiBold + ) + }, + shape = RoundedCornerShape(10.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = Color(0xFFF5F6F8), + unfocusedContainerColor = Color(0xFFF5F6F8), + disabledContainerColor = Color(0xFFF5F6F8), + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + focusedTextColor = DrexelBlue, + unfocusedTextColor = DrexelBlue + ), + singleLine = true + ) + } +} diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt index f6864d2..b43a64c 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt @@ -21,108 +21,121 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.Icon +import androidx.compose.runtime.* +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import kotlinx.coroutines.launch +import com.example.demo.feature.db.RideOffer +import com.example.demo.feature.db.RideRepository + @Composable -fun AvailableRidesScreen() { - // Dummy Data - val bestMatches = listOf( - RideOption("Abdul B.", 4.92, 11.71, 3, "Tesla Model Y (Blue)", "30th Street Station", "Cira Green", "Today 5:30 PM", true), - RideOption("Sarah M.", 4.87, 12.50, 2, "Honda Accord (Silver)", "30th Street Station", "Cira Green", "Today 5:45 PM", true), - RideOption("James K.", 4.85, 10.99, 4, "Toyota Camry (Black)", "30th Street Station", "Cira Green", "Today 6:00 PM", true) - ) - - val otherMatches = listOf( - RideOption("Lisa P.", 4.73, 14.25, 2, "Mazda CX-5 (Red)", "30th Street Station", "Cira Green", "Today 5:15 PM", false) - ) +fun AvailableRidesScreen( + modifier: Modifier = Modifier, + onBack: () -> Unit = {}, + rideRepository: RideRepository +) { + val coroutineScope = rememberCoroutineScope() + + var isLoading by remember { mutableStateOf(true) } + var errorMessage by remember { mutableStateOf(null) } + var rideOffers by remember { mutableStateOf>(emptyList()) } + + // load from DB on first composition + LaunchedEffect(Unit) { + try { + isLoading = true + errorMessage = null + rideOffers = rideRepository.getOpenRideOffers() + } catch (e: Exception) { + errorMessage = e.message ?: "Failed to load rides" + } finally { + isLoading = false + } + } + + // simple “best / other” split (first 3 vs rest) + val bestMatches: List = + if (rideOffers.size > 3) rideOffers.take(3) else rideOffers + val otherMatches: List = + if (rideOffers.size > 3) rideOffers.drop(3) else emptyList() Column( - modifier = Modifier + modifier = modifier .fillMaxSize() .background(FieldBackground) ) { - // Custom Header - Column( - modifier = Modifier - .fillMaxWidth() - .background(DrexelBlue) - .padding(start = 24.dp, end = 24.dp, top = 24.dp, bottom = 32.dp) - ) { - // Back Arrow and Filter Button - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - IconButton(onClick = { /* Handle Back */ }) { - Icon( - imageVector = Icons.Filled.ArrowBack, - contentDescription = "Back", - tint = Color.White - ) - } + // your existing header / back button code stays the same - Button( - onClick = { /* Handle Filter */ }, - colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1B4B6E)), // Slightly lighter blue - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), - shape = RoundedCornerShape(20.dp) - ) { - Icon( - imageVector = Icons.Filled.FilterList, - contentDescription = "Filter", - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Filter") - } + if (isLoading) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = DrexelBlue) } + return@Column + } - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "Available Rides", - style = MaterialTheme.typography.headlineMedium, - color = Color.White, - fontWeight = FontWeight.Bold - ) - - Text( - text = "7 rides found for your route", - style = MaterialTheme.typography.titleMedium, - color = Color.White.copy(alpha = 0.7f) - ) + if (errorMessage != null) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = errorMessage!!, + color = Color.Red + ) + } + return@Column } - // Scrollable List LazyColumn( - contentPadding = PaddingValues(16.dp), + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - // Best Matches - item { - SectionBadge(text = "Best Matches", color = DrexelGold, textColor = DrexelBlue) - Spacer(modifier = Modifier.height(8.dp)) - } + if (bestMatches.isNotEmpty()) { + item { + SectionBadge( + text = "Best Matches", + color = DrexelGold, + textColor = DrexelBlue + ) + Spacer(modifier = Modifier.height(8.dp)) + } - items(bestMatches) { ride -> - RideCard(ride) + items(bestMatches) { offer -> + RideCard(offer = offer) + } } - // Other Matches - item { - Spacer(modifier = Modifier.height(16.dp)) - SectionBadge(text = "Other Matches", color = Color.White, textColor = HintGrey, isBordered = true) - Spacer(modifier = Modifier.height(8.dp)) - } + if (otherMatches.isNotEmpty()) { + item { + Spacer(modifier = Modifier.height(16.dp)) + SectionBadge( + text = "Other Matches", + color = Color.White, + textColor = HintGrey, + isBordered = true + ) + Spacer(modifier = Modifier.height(8.dp)) + } - items(otherMatches) { ride -> - RideCard(ride) + items(otherMatches) { offer -> + RideCard(offer = offer) + } } } } } + + @Composable -fun RideCard(ride: RideOption) { +fun RideCard(offer: RideOffer) { Card( shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = Color.White), @@ -132,91 +145,54 @@ fun RideCard(ride: RideOption) { Column( modifier = Modifier.padding(16.dp) ) { + // You don't currently join driver name / rating / car model in RideOffer, + // so we’ll show what we *do* have and use placeholders where needed. + Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { - // Name, Star, Price, and Seats - Row(verticalAlignment = Alignment.CenterVertically) { + Column { Text( - text = ride.driverName, + text = "Driver #${offer.driverId}", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = DrexelBlue ) - - Spacer(modifier = Modifier.width(8.dp)) - - Icon( - imageVector = Icons.Filled.Star, - contentDescription = "Rating", - tint = DrexelGold, - modifier = Modifier.size(16.dp) - ) - Text( - text = ride.rating.toString(), - style = MaterialTheme.typography.bodyMedium, - color = HintGrey, - modifier = Modifier.padding(start = 4.dp) + text = "Vehicle #${offer.vehicleId}", + style = MaterialTheme.typography.bodySmall, + color = HintGrey ) } Column(horizontalAlignment = Alignment.End) { Text( - text = "$${ride.price}", + text = "$${"%.2f".format(offer.priceBase)}", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = DrexelBlue ) Text( - text = "${ride.seats} seats", - style = MaterialTheme.typography.labelSmall, + text = "${offer.seatsAvailable} seats", + style = MaterialTheme.typography.bodySmall, color = HintGrey ) } } - // Car Model + Spacer(modifier = Modifier.height(12.dp)) + Text( - text = ride.carModel, + text = "${offer.fromName} → ${offer.toName}", style = MaterialTheme.typography.bodyMedium, - color = HintGrey + color = DrexelBlue ) - Spacer(modifier = Modifier.height(16.dp)) - - // Route - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = ride.pickup, - style = MaterialTheme.typography.bodyMedium, - color = HintGrey - ) - - Spacer(modifier = Modifier.width(8.dp)) - - Icon( - imageVector = Icons.Filled.ArrowForward, - contentDescription = "to", - tint = HintGrey, - modifier = Modifier.size(14.dp) - ) - - Spacer(modifier = Modifier.width(8.dp)) - - Text( - text = ride.dropoff, - style = MaterialTheme.typography.bodyMedium, - color = HintGrey - ) - } - - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(8.dp)) - // Time Text( - text = ride.time, + text = offer.departAt, style = MaterialTheme.typography.bodyMedium, color = DrexelBlue ) diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt index 50bed4f..815342f 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt @@ -2,9 +2,9 @@ package com.example.demo.ui.theme import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -13,29 +13,28 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.demo.feature.rides.RideInput -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.Icon @Composable -fun OfferRideScreen() { +fun OfferRideScreen( + modifier: Modifier = Modifier, + onBack: () -> Unit = {}, + onPublish: () -> Unit = {} +) { val bgColor = Color(0xFFF3F4F6) - val scrollState = rememberScrollState() - Box( - modifier = Modifier + Column( + modifier = modifier .fillMaxSize() .background(bgColor) ) { - // 1. Header Section + // ---------- HEADER (smaller) ---------- Column( modifier = Modifier .fillMaxWidth() - .height(200.dp) .background(DrexelBlue) - .padding(24.dp) + .padding(horizontal = 20.dp, vertical = 16.dp) ) { - IconButton(onClick = { /* TODO: back nav */ }) { + IconButton(onClick = onBack) { Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = "Back", @@ -43,40 +42,41 @@ fun OfferRideScreen() { ) } - Spacer(modifier = Modifier.height(8.dp)) - Text( text = "Offer a Ride", - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleMedium, // smaller color = Color.White ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(4.dp)) Text( text = "Share your journey details", - style = MaterialTheme.typography.titleLarge, - color = HintGrey + style = MaterialTheme.typography.bodySmall, + color = Color.White.copy(alpha = 0.8f) ) } - // 2. The Floating Card + Spacer(modifier = Modifier.height(12.dp)) + + // ---------- CARD ---------- Card( modifier = Modifier .fillMaxWidth() - .padding(top = 175.dp, start = 16.dp, end = 16.dp, bottom = 16.dp) - .verticalScroll(scrollState), // Make form scrollable on small screens + .padding(horizontal = 16.dp), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = Color.White), - elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + elevation = CardDefaults.cardElevation(defaultElevation = 6.dp) ) { Column( - modifier = Modifier.padding(20.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) // tighter spacing ) { - // Locations + // Origin & Destination RideInput( - label = "Origin Location", + label = "Origin", icon = Icons.Filled.LocationOn, placeholder = "University Crossings" ) @@ -86,51 +86,46 @@ fun OfferRideScreen() { placeholder = "Korman Center" ) - // Departure Time - RideInput( - label = "Departure Time", - icon = Icons.Filled.AccessTime, - placeholder = "mm/dd/yyyy --:-- --", - ) - - // Vehicle Selection - RideInput( - label = "Choose Vehicle", - icon = Icons.Filled.DirectionsCar, - placeholder = "Tesla Model Y (Blue)", - ) - - // Available Seats - RideInput( - label = "Available Seats", - icon = Icons.Filled.Group, - placeholder = "2", - ) + // Time + Seats (2 per row to save height) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + RideInput( + label = "Time", + icon = Icons.Filled.AccessTime, + placeholder = "mm/dd hh:mm", + modifier = Modifier.weight(1f) + ) + RideInput( + label = "Seats", + icon = Icons.Filled.Group, + placeholder = "2", + modifier = Modifier.weight(1f) + ) + } - // Base Price & Price Per Mile - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + // Vehicle + Price (2 per row) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { RideInput( - label = "Base Price", - icon = Icons.Filled.AttachMoney, - placeholder = "5.00", + label = "Vehicle", + icon = Icons.Filled.DirectionsCar, + placeholder = "Model Y", modifier = Modifier.weight(1f) ) RideInput( - label = "Per Mile", + label = "Price", icon = Icons.Filled.AttachMoney, - placeholder = "0.50", + placeholder = "5.00", modifier = Modifier.weight(1f) ) } Spacer(modifier = Modifier.height(4.dp)) - // Publish Button + // ---------- BUTTON (always visible) ---------- Button( - onClick = { /* TODO: publish offer */ }, + onClick = onPublish, modifier = Modifier .fillMaxWidth() - .height(50.dp), + .height(44.dp), // a bit shorter colors = ButtonDefaults.buttonColors( containerColor = DrexelGold, contentColor = DrexelBlue @@ -139,11 +134,14 @@ fun OfferRideScreen() { ) { Text( text = "Publish Offer", - fontWeight = FontWeight.Bold, - fontSize = 16.sp + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp // smaller ) } } } + + // tiny spacer so it doesn’t touch bottom nav + Spacer(modifier = Modifier.height(8.dp)) } } From c6bc3fb8183d9c91bfd9d804df9be5c705121c7a Mon Sep 17 00:00:00 2001 From: Samii Shabuse Date: Mon, 1 Dec 2025 21:06:18 -0500 Subject: [PATCH 31/31] fixed db for profile page to show crud --- .../com/example/demo/FindMyRideDbProvider.kt | 10 +++- .../kotlin/com/example/demo/MainActivity.kt | 4 +- .../demo/feature/profile/ProfileScreen.kt | 2 +- .../demo/feature/profile/ProfileViewModel.kt | 60 ++++++++++++++----- 4 files changed, 55 insertions(+), 21 deletions(-) diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt index 3da45bc..6adb08a 100644 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt @@ -9,12 +9,18 @@ class FindMyRideDbProvider(private val context: Context) { private fun ensureDbCopied() { val dbFile = context.getDatabasePath(dbName) - if (dbFile.exists()) return + // Always overwrite old database to ensure latest version is used dbFile.parentFile?.mkdirs() + // Delete any existing DB + if (dbFile.exists()) { + dbFile.delete() + } + + // Copy fresh DB from assets context.assets.open(dbName).use { input -> - FileOutputStream(dbFile).use { output -> + dbFile.outputStream().use { output -> input.copyTo(output) } } diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt index 628becb..9ff986d 100644 --- a/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt +++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/MainActivity.kt @@ -11,14 +11,14 @@ import com.example.demo.App import com.example.demo.AndroidRideRepository import com.example.demo.AndroidAuthRepository import com.example.demo.FindMyRideDbProvider +import com.example.demo.CurrentUserStore import com.example.demo.feature.profile.data.AndroidProfileRepository import com.example.demo.feature.messages.data.AndroidMessagesRepository -import com.example.demo.feature.rides.AvailableRidesScreen -import com.example.demo.ui.theme.FindRideScreen class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setContent { MaterialTheme { Surface { diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/profile/ProfileScreen.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/profile/ProfileScreen.kt index 58ba576..cbc6fc5 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/profile/ProfileScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/profile/ProfileScreen.kt @@ -38,7 +38,7 @@ enum class ProfilePage { @Composable fun ProfileRoute( modifier: Modifier = Modifier, - repository: ProfileRepository = InMemoryProfileRepository() + repository: ProfileRepository ) { val viewModel = remember(repository) { ProfileViewModel(repository) } var currentPage by remember { mutableStateOf(ProfilePage.Overview) } diff --git a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/profile/ProfileViewModel.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/profile/ProfileViewModel.kt index 0d3397a..6d063f1 100644 --- a/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/profile/ProfileViewModel.kt +++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/profile/ProfileViewModel.kt @@ -7,7 +7,7 @@ import com.example.demo.feature.profile.data.InMemoryProfileRepository import com.example.demo.feature.profile.data.ProfileRepository class ProfileViewModel( - private val repository: ProfileRepository = InMemoryProfileRepository() + private val repository: ProfileRepository ) { private val _uiState = MutableStateFlow(repository.loadInitialProfile()) @@ -180,37 +180,65 @@ class ProfileViewModel( ) } - private fun saveVehicle() { + fun saveVehicle() { val state = _uiState.value val edit = state.vehicleEdit - val seats = edit.seatsTotal.toIntOrNull() ?: 0 - val year = edit.year.toIntOrNull() ?: 0 + // --- Enforce DB constraints safely --- + + // Seats: parse, clamp 1..8, default to 4 if blank/bad + val seats = edit.seatsTotal + .toIntOrNull() + ?.coerceIn(1, 8) + ?: 4 + + // Year: parse, clamp 1900..2100, default to 2024 if blank/bad + val safeYear = edit.year + .toIntOrNull() + ?.coerceIn(1900, 2100) + ?: 2024 + + // Required strings: make sure they are not blank + val safeMake = edit.make.ifBlank { "Unknown" } + val safeModel = edit.model.ifBlank { "Car" } + val safeColor = edit.color.ifBlank { "Unknown" } + val basePlate = edit.plate.ifBlank { "TEMP" } val updatedVehicles = if (edit.id == null) { - val newId = (state.vehicles.maxOfOrNull { it.id } ?: 0) + 1 + // New vehicle → CREATE + // Use negative IDs so we never collide with existing positive DB IDs + val currentMinId = state.vehicles.minOfOrNull { it.id } ?: 0 + val newId = if (currentMinId > 0) -1 else currentMinId - 1 + + // Make sure plate is unique-ish if user left it blank + val safePlate = if (edit.plate.isBlank()) { + "$basePlate-$newId" // <-- use whatever base string you defined above + } else { + edit.plate + } state.vehicles + VehicleUi( id = newId, - ownerUserId = edit.ownerUserId ?: 1, - make = edit.make, - model = edit.model, - color = edit.color, - plate = edit.plate, + ownerUserId = edit.ownerUserId ?: 1, // current user id placeholder + make = safeMake, + model = safeModel, + color = safeColor, + plate = safePlate, seatsTotal = seats, - year = year, + year = safeYear, funFact = edit.funFact ) } else { + // Existing vehicle → UPDATE state.vehicles.map { v -> if (v.id == edit.id) { v.copy( - make = edit.make, - model = edit.model, - color = edit.color, - plate = edit.plate, + make = safeMake, + model = safeModel, + color = safeColor, + plate = if (edit.plate.isBlank()) v.plate else edit.plate, seatsTotal = seats, - year = year, + year = safeYear, funFact = edit.funFact ) } else v