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/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index 797d989..87b2916 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -30,6 +30,12 @@
+
+
+
+
+
+
\ No newline at end of file
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
-[](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.
+
+
+-------------------------------------------------------------
+
+
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/assets/findmyride.db b/app/composeApp/src/androidMain/assets/findmyride.db
new file mode 100644
index 0000000..7019dce
Binary files /dev/null and b/app/composeApp/src/androidMain/assets/findmyride.db differ
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..8e74451
--- /dev/null
+++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidAuthRepository.kt
@@ -0,0 +1,116 @@
+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
+
+/**
+ * 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, email, username, role
+ FROM "USER"
+ WHERE email = ? AND password_hash = ?
+ """.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"))
+ }
+ }
+ }
+
+ // 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/AndroidMessagesRepository.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidMessagesRepository.kt
new file mode 100644
index 0000000..600f517
--- /dev/null
+++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidMessagesRepository.kt
@@ -0,0 +1,239 @@
+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
+ 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.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,
+ 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(),
+ 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)
+ val sentAt = c.getString(idxLastSentAt) ?: ""
+
+ result += MessageThreadUi(
+ id = c.getInt(idxThreadId),
+ senderName = name,
+ initials = initialsFromName(name),
+ lastMessage = lastMsg,
+ timeAgo = sentAt,
+ 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/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/AndroidRideRepository.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideRepository.kt
new file mode 100644
index 0000000..335ba12
--- /dev/null
+++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/AndroidRideRepository.kt
@@ -0,0 +1,117 @@
+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)
+ }
+ }
+
+ 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/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/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt
new file mode 100644
index 0000000..6adb08a
--- /dev/null
+++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/FindMyRideDbProvider.kt
@@ -0,0 +1,48 @@
+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)
+
+ // 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 ->
+ dbFile.outputStream().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_READWRITE
+ )
+ }
+}
\ 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..9ff986d 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,43 @@
-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.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
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
- enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
- App()
+ 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
+ )
+ }
+ }
}
}
}
-
-@Preview
-@Composable
-fun AppAndroidPreview() {
- App()
-}
\ No newline at end of file
diff --git a/app/composeApp/src/androidMain/kotlin/com/example/demo/RideShareDbHelper.kt b/app/composeApp/src/androidMain/kotlin/com/example/demo/RideShareDbHelper.kt
new file mode 100644
index 0000000..777c33d
--- /dev/null
+++ b/app/composeApp/src/androidMain/kotlin/com/example/demo/RideShareDbHelper.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/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/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/App.kt b/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt
index 6ed3ec2..129cb15 100644
--- a/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt
+++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/App.kt
@@ -2,11 +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 {
@@ -17,7 +20,12 @@ enum class RootScreen {
}
@Composable
-fun App() {
+fun App(
+ rideRepository: RideRepository,
+ profileRepository: ProfileRepository,
+ authRepository: AuthRepository,
+ messagesRepository: MessagesRepository
+) {
var currentScreen by remember { mutableStateOf(RootScreen.Login) }
MaterialTheme {
@@ -26,7 +34,8 @@ fun App() {
RootScreen.Login -> LoginRoute(
onNavigateToSignUp = { currentScreen = RootScreen.SignUp },
onNavigateToForgotPassword = { currentScreen = RootScreen.ForgotPassword },
- onLoginSuccess = { currentScreen = RootScreen.Main }
+ onLoginSuccess = { currentScreen = RootScreen.Main },
+ authRepository = authRepository,
)
RootScreen.SignUp -> SignUpRoute(
@@ -37,7 +46,12 @@ fun App() {
onNavigateBack = { currentScreen = RootScreen.Login }
)
- RootScreen.Main -> MainRoute()
+ RootScreen.Main -> MainRoute(
+ rideRepository = rideRepository,
+ profileRepository = profileRepository,
+ messagesRepository = messagesRepository,
+
+ )
}
}
}
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
}
}
-
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..d61f199
--- /dev/null
+++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/db/RideRepository.kt
@@ -0,0 +1,35 @@
+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
+ )
+
+ 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/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 c4f681b..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
@@ -9,49 +9,92 @@ 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.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() {
- var currentTab by remember { mutableStateOf(MainTab.Home) } // start on profile for now
+fun MainRoute(
+ rideRepository: RideRepository,
+ profileRepository: ProfileRepository,
+ messagesRepository: MessagesRepository,
+) {
+ 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 -> {
- // TODO: replace with real HomeRoute
- Text(
- text = "Home screen placeholder",
- modifier = Modifier.padding(padding)
- )
+ 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 -> {
- Text(
- text = "My Rides placeholder",
+ MyRidesScreen(
modifier = Modifier.padding(padding)
)
}
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
+ )
}
}
}
}
+
@Composable
private fun MainBottomNav(
currentTab: MainTab,
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)
+ )
+ }
+}
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..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
@@ -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,20 +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
if (thread == null) {
@@ -33,6 +37,7 @@ fun MessagesRoute(
threadId = thread.id,
contactName = thread.senderName,
initials = thread.initials,
+ repository = repository,
onBack = { currentPage = MessagesPage.List },
modifier = modifier
)
@@ -40,3 +45,4 @@ fun MessagesRoute(
}
}
}
+
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..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,13 +2,23 @@ package com.example.demo.feature.messages.list
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
+import com.example.demo.feature.messages.data.MessagesRepository
@Composable
fun MessagesListRoute(
modifier: Modifier = Modifier,
onOpenConversation: (MessageThreadUi) -> Unit,
- viewModel: MessagesViewModel = remember { MessagesViewModel() }
+ repository: MessagesRepository
) {
+ // For now, we use userId = 1; AndroidMessagesRepository internally
+ // will override this with CurrentUserStore.userId when available.
+ val viewModel = remember(repository) {
+ MessagesViewModel(
+ repository = repository,
+ userId = 1
+ )
+ }
+
val state by viewModel.uiState.collectAsState()
MessagesScreen(
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 {
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..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
@@ -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
) {
+ 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
)
-
}
}
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
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..58d8143
--- /dev/null
+++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableOffersScreen.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.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
new file mode 100644
index 0000000..b43a64c
--- /dev/null
+++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/AvailableRidesScreen.kt
@@ -0,0 +1,224 @@
+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 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 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(
+ 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
+ .fillMaxSize()
+ .background(FieldBackground)
+ ) {
+ // your existing header / back button code stays the same
+
+ if (isLoading) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(color = DrexelBlue)
+ }
+ return@Column
+ }
+
+ if (errorMessage != null) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = errorMessage!!,
+ color = Color.Red
+ )
+ }
+ return@Column
+ }
+
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ if (bestMatches.isNotEmpty()) {
+ item {
+ SectionBadge(
+ text = "Best Matches",
+ color = DrexelGold,
+ textColor = DrexelBlue
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+
+ items(bestMatches) { offer ->
+ RideCard(offer = offer)
+ }
+ }
+
+ 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) { offer ->
+ RideCard(offer = offer)
+ }
+ }
+ }
+ }
+}
+
+
+
+@Composable
+fun RideCard(offer: RideOffer) {
+ 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)
+ ) {
+ // 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
+ ) {
+ Column {
+ Text(
+ text = "Driver #${offer.driverId}",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = DrexelBlue
+ )
+ Text(
+ text = "Vehicle #${offer.vehicleId}",
+ style = MaterialTheme.typography.bodySmall,
+ color = HintGrey
+ )
+ }
+
+ Column(horizontalAlignment = Alignment.End) {
+ Text(
+ text = "$${"%.2f".format(offer.priceBase)}",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = DrexelBlue
+ )
+ Text(
+ text = "${offer.seatsAvailable} seats",
+ style = MaterialTheme.typography.bodySmall,
+ color = HintGrey
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Text(
+ text = "${offer.fromName} → ${offer.toName}",
+ style = MaterialTheme.typography.bodyMedium,
+ color = DrexelBlue
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = offer.departAt,
+ 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
+ )
+ }
+}
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..baf2f79
--- /dev/null
+++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FieldInputs.kt
@@ -0,0 +1,80 @@
+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.graphics.vector.ImageVector
+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: ImageVector,
+ 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(
+ imageVector = 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
new file mode 100644
index 0000000..83313d1
--- /dev/null
+++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/FindRideScreen.kt
@@ -0,0 +1,144 @@
+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.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 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() {
+ 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 = { /* TODO: handle back nav */ }) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ 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
+ )
+ }
+
+ // 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 = "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 = Icons.Filled.CalendarMonth,
+ placeholder = "mm/dd/yyyy",
+ modifier = Modifier.weight(1f)
+ )
+ RideInput(
+ label = "Time",
+ icon = Icons.Filled.AccessTime,
+ placeholder = "--:-- --",
+ modifier = Modifier.weight(1f)
+ )
+ }
+
+ // Seats & Price
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ RideInput(
+ label = "Seats Needed",
+ icon = Icons.Filled.Group,
+ placeholder = "2",
+ modifier = Modifier.weight(1f)
+ )
+ RideInput(
+ label = "Max Price",
+ icon = Icons.Filled.AttachMoney,
+ placeholder = "20",
+ modifier = Modifier.weight(1f)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ // Search Button
+ Button(
+ onClick = { /* TODO: trigger search */ },
+ 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
+ )
+ }
+ }
+ }
+ }
+}
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..815342f
--- /dev/null
+++ b/app/composeApp/src/commonMain/kotlin/com/example/demo/feature/rides/OfferRideScreen.kt
@@ -0,0 +1,147 @@
+package com.example.demo.ui.theme
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+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
+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.feature.rides.RideInput
+
+@Composable
+fun OfferRideScreen(
+ modifier: Modifier = Modifier,
+ onBack: () -> Unit = {},
+ onPublish: () -> Unit = {}
+) {
+ val bgColor = Color(0xFFF3F4F6)
+
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .background(bgColor)
+ ) {
+ // ---------- HEADER (smaller) ----------
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(DrexelBlue)
+ .padding(horizontal = 20.dp, vertical = 16.dp)
+ ) {
+ IconButton(onClick = onBack) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ contentDescription = "Back",
+ tint = Color.White
+ )
+ }
+
+ Text(
+ text = "Offer a Ride",
+ style = MaterialTheme.typography.titleMedium, // smaller
+ color = Color.White
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Text(
+ text = "Share your journey details",
+ style = MaterialTheme.typography.bodySmall,
+ color = Color.White.copy(alpha = 0.8f)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // ---------- CARD ----------
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(containerColor = Color.White),
+ elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp) // tighter spacing
+ ) {
+ // Origin & Destination
+ RideInput(
+ label = "Origin",
+ icon = Icons.Filled.LocationOn,
+ placeholder = "University Crossings"
+ )
+ RideInput(
+ label = "Destination",
+ icon = Icons.Filled.LocationOn,
+ placeholder = "Korman Center"
+ )
+
+ // 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)
+ )
+ }
+
+ // Vehicle + Price (2 per row)
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ RideInput(
+ label = "Vehicle",
+ icon = Icons.Filled.DirectionsCar,
+ placeholder = "Model Y",
+ modifier = Modifier.weight(1f)
+ )
+ RideInput(
+ label = "Price",
+ icon = Icons.Filled.AttachMoney,
+ placeholder = "5.00",
+ modifier = Modifier.weight(1f)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ // ---------- BUTTON (always visible) ----------
+ Button(
+ onClick = onPublish,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(44.dp), // a bit shorter
+ colors = ButtonDefaults.buttonColors(
+ containerColor = DrexelGold,
+ contentColor = DrexelBlue
+ ),
+ shape = RoundedCornerShape(8.dp)
+ ) {
+ Text(
+ text = "Publish Offer",
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 14.sp // smaller
+ )
+ }
+ }
+ }
+
+ // tiny spacer so it doesn’t touch bottom nav
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+}
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
diff --git a/docs/dev_journal/Kennan.md b/docs/dev_journal/Kennan.md
index a772d0d..01e91bf 100644
--- a/docs/dev_journal/Kennan.md
+++ b/docs/dev_journal/Kennan.md
@@ -1,3 +1,52 @@
+# 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
+
+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/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
diff --git a/identifier.sqlite b/identifier.sqlite
new file mode 100644
index 0000000..e69de29