diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/HomeTitleScreen.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/HomeTitleScreen.kt new file mode 100644 index 0000000..2cf4fe3 --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/HomeTitleScreen.kt @@ -0,0 +1,264 @@ +package com.shieldai.android.ui.screens.services + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.shieldai.android.R +import com.shieldai.android.ui.components.BadgeVariant +import com.shieldai.android.ui.components.ShieldBadge +import com.shieldai.android.ui.components.ShieldButton +import com.shieldai.android.ui.components.ShieldButtonVariant +import com.shieldai.android.ui.components.ShieldCard +import com.shieldai.android.ui.components.ShieldEmptyState +import com.shieldai.android.ui.components.ShieldTextField +import com.shieldai.android.viewmodel.HomeTitleViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeTitleScreen( + onBack: () -> Unit = {}, + modifier: Modifier = Modifier, + viewModel: HomeTitleViewModel = viewModel(factory = HomeTitleViewModel.Factory) +) { + val uiState by viewModel.uiState.collectAsState() + var showAddSheet by remember { mutableStateOf(false) } + var newAddress by remember { mutableStateOf("") } + + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState() + ) + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + LargeTopAppBar( + title = { Text("HomeTitle", fontWeight = FontWeight.SemiBold) }, + navigationIcon = { + TextButton(onClick = onBack) { Text("Back") } + }, + scrollBehavior = scrollBehavior + ) + }, + floatingActionButton = { + if (!showAddSheet) { + FloatingActionButton(onClick = { showAddSheet = true }) { + Icon( + painter = painterResource(R.drawable.ic_dashboard), + contentDescription = "Add property" + ) + } + } + } + ) { paddingValues -> + when { + uiState.isLoading && uiState.properties.isEmpty() -> { + androidx.compose.foundation.layout.Box( + modifier = Modifier.fillMaxSize().padding(paddingValues), + contentAlignment = androidx.compose.ui.Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } + } + uiState.properties.isEmpty() -> { + ShieldEmptyState( + title = "No properties", + description = "Add properties to monitor for title fraud", + actionButton = { + ShieldButton( + text = "Add Property", + onClick = { showAddSheet = true }, + variant = ShieldButtonVariant.Primary + ) + }, + modifier = Modifier.padding(paddingValues) + ) + } + else -> { + HomeTitleContent( + uiState = uiState, + modifier = Modifier.padding(paddingValues) + ) + } + } + + if (showAddSheet) { + AddPropertySheet( + onDismiss = { + showAddSheet = false + newAddress = "" + }, + onAdd = { + viewModel.addProperty(newAddress) + showAddSheet = false + newAddress = "" + }, + address = newAddress, + onAddressChange = { newAddress = it }, + isLoading = uiState.isAdding + ) + } + } +} + +@Composable +private fun HomeTitleContent( + uiState: HomeTitleViewModel.HomeTitleUiState, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { + Text( + text = "Properties (${uiState.properties.size})", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + items(uiState.properties) { property -> + PropertyCard(property) + Spacer(modifier = Modifier.height(8.dp)) + } + } +} + +@Composable +private fun PropertyCard(property: com.shieldai.android.data.model.Property) { + ShieldCard(modifier = Modifier.fillMaxWidth()) { + Column { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = property.address, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + property.ownerName?.let { + Text( + text = "Owner: $it", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + ShieldBadge( + text = property.type, + variant = BadgeVariant.Info + ) + property.county?.let { + ShieldBadge( + text = it, + variant = BadgeVariant.Default + ) + } + } + } + ShieldBadge( + text = property.status, + variant = if (property.status == "monitored") BadgeVariant.Success + else BadgeVariant.Default + ) + } + property.updatedAt?.let { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Updated: $it", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AddPropertySheet( + onDismiss: () -> Unit, + onAdd: () -> Unit, + address: String, + onAddressChange: (String) -> Unit, + isLoading: Boolean +) { + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Add Property", + style = MaterialTheme.typography.titleLarge + ) + ShieldTextField( + value = address, + onValueChange = onAddressChange, + label = "Property address", + modifier = Modifier.fillMaxWidth() + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + ShieldButton( + text = "Cancel", + onClick = onDismiss, + variant = ShieldButtonVariant.Secondary, + modifier = Modifier.weight(1f) + ) + ShieldButton( + text = "Add", + onClick = onAdd, + variant = ShieldButtonVariant.Primary, + modifier = Modifier.weight(1f), + enabled = address.isNotBlank(), + loading = isLoading + ) + } + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/RemoveBrokersScreen.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/RemoveBrokersScreen.kt new file mode 100644 index 0000000..7550c0e --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/RemoveBrokersScreen.kt @@ -0,0 +1,327 @@ +package com.shieldai.android.ui.screens.services + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.shieldai.android.R +import com.shieldai.android.ui.components.BadgeVariant +import com.shieldai.android.ui.components.ShieldBadge +import com.shieldai.android.ui.components.ShieldButton +import com.shieldai.android.ui.components.ShieldButtonVariant +import com.shieldai.android.ui.components.ShieldCard +import com.shieldai.android.ui.components.ShieldEmptyState +import com.shieldai.android.ui.components.ShieldTextField +import com.shieldai.android.viewmodel.RemoveBrokersViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RemoveBrokersScreen( + onBack: () -> Unit = {}, + modifier: Modifier = Modifier, + viewModel: RemoveBrokersViewModel = viewModel(factory = RemoveBrokersViewModel.Factory) +) { + val uiState by viewModel.uiState.collectAsState() + var showCreateSheet by remember { mutableStateOf(false) } + var selectedListingId by remember { mutableStateOf("") } + var selectedListingName by remember { mutableStateOf("") } + var notes by remember { mutableStateOf("") } + + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState() + ) + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + LargeTopAppBar( + title = { Text("RemoveBrokers", fontWeight = FontWeight.SemiBold) }, + navigationIcon = { + TextButton(onClick = onBack) { Text("Back") } + }, + scrollBehavior = scrollBehavior + ) + }, + floatingActionButton = { + if (!showCreateSheet) { + FloatingActionButton(onClick = { showCreateSheet = true }) { + Icon( + painter = painterResource(R.drawable.ic_dashboard), + contentDescription = "Start removal" + ) + } + } + } + ) { paddingValues -> + when { + uiState.isLoading && uiState.listings.isEmpty() -> { + androidx.compose.foundation.layout.Box( + modifier = Modifier.fillMaxSize().padding(paddingValues), + contentAlignment = androidx.compose.ui.Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } + } + uiState.listings.isEmpty() && uiState.removalRequests.isEmpty() -> { + ShieldEmptyState( + title = "No listings", + description = "No broker listings found. Start a removal request to get started.", + actionButton = { + ShieldButton( + text = "Start Removal", + onClick = { showCreateSheet = true }, + variant = ShieldButtonVariant.Primary + ) + }, + modifier = Modifier.padding(paddingValues) + ) + } + else -> { + RemoveBrokersContent( + uiState = uiState, + modifier = Modifier.padding(paddingValues) + ) + } + } + + if (showCreateSheet) { + CreateRemovalSheet( + onDismiss = { + showCreateSheet = false + selectedListingId = "" + selectedListingName = "" + notes = "" + }, + onCreate = { + viewModel.createRemovalRequest(selectedListingId, notes.ifBlank { null }) + showCreateSheet = false + selectedListingId = "" + selectedListingName = "" + notes = "" + }, + listingName = selectedListingName, + onListingNameChange = { selectedListingName = it }, + notes = notes, + onNotesChange = { notes = it }, + isLoading = uiState.isCreating + ) + } + } +} + +@Composable +private fun RemoveBrokersContent( + uiState: RemoveBrokersViewModel.RemoveBrokersUiState, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + if (uiState.listings.isNotEmpty()) { + item { + Text( + text = "Broker Listings (${uiState.listings.size})", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + items(uiState.listings) { listing -> + ListingCard(listing) + Spacer(modifier = Modifier.height(8.dp)) + } + } + + if (uiState.removalRequests.isNotEmpty()) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Removal Requests (${uiState.removalRequests.size})", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + items(uiState.removalRequests) { request -> + RemovalRequestCard(request) + Spacer(modifier = Modifier.height(8.dp)) + } + } + } +} + +@Composable +private fun ListingCard(listing: com.shieldai.android.data.model.BrokerListing) { + ShieldCard(modifier = Modifier.fillMaxWidth()) { + Column { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = listing.brokerName, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + modifier = Modifier.weight(1f) + ) + ShieldBadge( + text = listing.status, + variant = if (listing.status == "active") BadgeVariant.Warning + else BadgeVariant.Default + ) + } + listing.propertyAddress?.let { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + listing.dateFound?.let { + Text( + text = "Found: $it", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + +@Composable +private fun RemovalRequestCard(request: com.shieldai.android.data.model.RemovalRequest) { + ShieldCard(modifier = Modifier.fillMaxWidth()) { + Column { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Request #${request.id.take(8)}", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + modifier = Modifier.weight(1f) + ) + ShieldBadge( + text = request.status, + variant = when (request.status.lowercase()) { + "completed" -> BadgeVariant.Success + "pending" -> BadgeVariant.Warning + "in_progress" -> BadgeVariant.Info + else -> BadgeVariant.Default + } + ) + } + request.submittedDate?.let { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Submitted: $it", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + request.notes?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CreateRemovalSheet( + onDismiss: () -> Unit, + onCreate: () -> Unit, + listingName: String, + onListingNameChange: (String) -> Unit, + notes: String, + onNotesChange: (String) -> Unit, + isLoading: Boolean +) { + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Start Removal Request", + style = MaterialTheme.typography.titleLarge + ) + ShieldTextField( + value = listingName, + onValueChange = onListingNameChange, + label = "Broker / Listing name", + modifier = Modifier.fillMaxWidth() + ) + ShieldTextField( + value = notes, + onValueChange = onNotesChange, + label = "Notes (optional)", + modifier = Modifier.fillMaxWidth() + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + ShieldButton( + text = "Cancel", + onClick = onDismiss, + variant = ShieldButtonVariant.Secondary, + modifier = Modifier.weight(1f) + ) + ShieldButton( + text = "Submit", + onClick = onCreate, + variant = ShieldButtonVariant.Primary, + modifier = Modifier.weight(1f), + enabled = listingName.isNotBlank(), + loading = isLoading + ) + } + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/SpamShieldScreen.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/SpamShieldScreen.kt new file mode 100644 index 0000000..d409e5e --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/SpamShieldScreen.kt @@ -0,0 +1,338 @@ +package com.shieldai.android.ui.screens.services + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.shieldai.android.R +import com.shieldai.android.ui.components.ShieldBadge +import com.shieldai.android.ui.components.ShieldButton +import com.shieldai.android.ui.components.ShieldButtonVariant +import com.shieldai.android.ui.components.ShieldCard +import com.shieldai.android.ui.components.ShieldEmptyState +import com.shieldai.android.ui.components.ShieldTextField +import com.shieldai.android.viewmodel.SpamShieldViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SpamShieldScreen( + onBack: () -> Unit = {}, + modifier: Modifier = Modifier, + viewModel: SpamShieldViewModel = viewModel(factory = SpamShieldViewModel.Factory) +) { + val uiState by viewModel.uiState.collectAsState() + var showCreateSheet by remember { mutableStateOf(false) } + var newPattern by remember { mutableStateOf("") } + var newAction by remember { mutableStateOf("block") } + var newDescription by remember { mutableStateOf("") } + + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState() + ) + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + LargeTopAppBar( + title = { Text("SpamShield", fontWeight = FontWeight.SemiBold) }, + navigationIcon = { + TextButton(onClick = onBack) { Text("Back") } + }, + scrollBehavior = scrollBehavior + ) + }, + floatingActionButton = { + if (!showCreateSheet) { + FloatingActionButton(onClick = { showCreateSheet = true }) { + Icon( + painter = painterResource(R.drawable.ic_dashboard), + contentDescription = "Create rule" + ) + } + } + } + ) { paddingValues -> + when { + uiState.isLoading && uiState.rules.isEmpty() -> { + androidx.compose.foundation.layout.Box( + modifier = Modifier.fillMaxSize().padding(paddingValues), + contentAlignment = androidx.compose.ui.Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } + } + else -> { + SpamShieldContent( + uiState = uiState, + modifier = Modifier.padding(paddingValues) + ) + } + } + + if (showCreateSheet) { + CreateRuleSheet( + onDismiss = { + showCreateSheet = false + newPattern = "" + newDescription = "" + }, + onCreate = { + viewModel.createRule(newPattern, newAction, newDescription.ifBlank { null }) + showCreateSheet = false + newPattern = "" + newDescription = "" + }, + pattern = newPattern, + onPatternChange = { newPattern = it }, + action = newAction, + onActionChange = { newAction = it }, + description = newDescription, + onDescriptionChange = { newDescription = it }, + isLoading = uiState.isCreating + ) + } + } +} + +@Composable +private fun SpamShieldContent( + uiState: SpamShieldViewModel.SpamShieldUiState, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { + SpamStatsRow( + blocked = uiState.totalBlocked, + flagged = uiState.totalFlagged, + active = uiState.activeRules + ) + } + + if (uiState.rules.isNotEmpty()) { + item { + Text( + text = "Rules (${uiState.rules.size})", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + items(uiState.rules) { rule -> + RuleCard(rule) { enabled -> + viewModel.toggleRule(rule.id, enabled) + } + Spacer(modifier = Modifier.height(8.dp)) + } + } else { + item { + ShieldEmptyState( + title = "No rules", + description = "Create spam filtering rules to protect your phone", + actionButton = { + ShieldButton( + text = "Create Rule", + onClick = { /* handled by parent */ }, + variant = ShieldButtonVariant.Primary + ) + } + ) + } + } + } +} + +@Composable +private fun SpamStatsRow( + blocked: Int, + flagged: Int, + active: Int +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + StatCard("Blocked", blocked, modifier = Modifier.weight(1f)) + StatCard("Flagged", flagged, modifier = Modifier.weight(1f)) + StatCard("Active", active, modifier = Modifier.weight(1f)) + } +} + +@Composable +private fun StatCard( + label: String, + value: Int, + modifier: Modifier = Modifier +) { + ShieldCard( + modifier = modifier + ) { + Column( + horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally + ) { + Text( + text = "$value", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = label, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +private fun RuleCard( + rule: com.shieldai.android.data.model.SpamRule, + onToggle: (Boolean) -> Unit +) { + ShieldCard(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = rule.pattern, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + ShieldBadge( + text = rule.action, + variant = if (rule.action == "block") com.shieldai.android.ui.components.BadgeVariant.Error + else com.shieldai.android.ui.components.BadgeVariant.Warning + ) + if (rule.priority > 0) { + ShieldBadge( + text = "P${rule.priority}", + variant = com.shieldai.android.ui.components.BadgeVariant.Info + ) + } + } + rule.description?.let { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + Switch( + checked = rule.enabled, + onCheckedChange = onToggle + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CreateRuleSheet( + onDismiss: () -> Unit, + onCreate: () -> Unit, + pattern: String, + onPatternChange: (String) -> Unit, + action: String, + onActionChange: (String) -> Unit, + description: String, + onDescriptionChange: (String) -> Unit, + isLoading: Boolean +) { + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Create Spam Rule", + style = MaterialTheme.typography.titleLarge + ) + ShieldTextField( + value = pattern, + onValueChange = onPatternChange, + label = "Pattern (phone number or keyword)", + modifier = Modifier.fillMaxWidth() + ) + ShieldTextField( + value = action, + onValueChange = onActionChange, + label = "Action (block, flag, log)", + modifier = Modifier.fillMaxWidth() + ) + ShieldTextField( + value = description, + onValueChange = onDescriptionChange, + label = "Description (optional)", + modifier = Modifier.fillMaxWidth() + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + ShieldButton( + text = "Cancel", + onClick = onDismiss, + variant = ShieldButtonVariant.Secondary, + modifier = Modifier.weight(1f) + ) + ShieldButton( + text = "Create", + onClick = onCreate, + variant = ShieldButtonVariant.Primary, + modifier = Modifier.weight(1f), + enabled = pattern.isNotBlank(), + loading = isLoading + ) + } + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/settings/SettingsScreen.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/settings/SettingsScreen.kt new file mode 100644 index 0000000..9ba1d78 --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/settings/SettingsScreen.kt @@ -0,0 +1,346 @@ +package com.shieldai.android.ui.screens.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.shieldai.android.ui.components.ShieldAvatar +import com.shieldai.android.ui.components.ShieldBadge +import com.shieldai.android.ui.components.ShieldButton +import com.shieldai.android.ui.components.ShieldButtonVariant +import com.shieldai.android.ui.components.ShieldCard +import com.shieldai.android.ui.components.ShieldEmptyState +import com.shieldai.android.viewmodel.AuthViewModel +import com.shieldai.android.viewmodel.SettingsViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen( + onBack: () -> Unit = {}, + modifier: Modifier = Modifier, + viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory), + authViewModel: AuthViewModel = viewModel(factory = AuthViewModel.Factory) +) { + val uiState by viewModel.uiState.collectAsState() + var showLogoutDialog by remember { mutableStateOf(false) } + + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState() + ) + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + LargeTopAppBar( + title = { Text("Settings", fontWeight = FontWeight.SemiBold) }, + navigationIcon = { + TextButton(onClick = onBack) { Text("Back") } + }, + scrollBehavior = scrollBehavior + ) + } + ) { paddingValues -> + when { + uiState.isLoading -> { + androidx.compose.foundation.layout.Box( + modifier = Modifier.fillMaxSize().padding(paddingValues), + contentAlignment = androidx.compose.ui.Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } + } + uiState.user == null -> { + ShieldEmptyState( + title = "Failed to load settings", + description = uiState.error ?: "Unable to load your settings", + actionButton = { + TextButton(onClick = { viewModel.refresh() }) { + Text("Retry") + } + }, + modifier = Modifier.padding(paddingValues) + ) + } + else -> { + SettingsContent( + uiState = uiState, + onToggleNotifications = { viewModel.toggleNotifications(it) }, + onToggleDarkMode = { viewModel.toggleDarkMode(it) }, + onToggleBiometric = { viewModel.toggleBiometric(it) }, + onUpgradeSubscription = { viewModel.upgradeSubscription() }, + onShowLogoutDialog = { showLogoutDialog = true }, + modifier = Modifier.padding(paddingValues) + ) + } + } + + if (showLogoutDialog) { + androidx.compose.material3.AlertDialog( + onDismissRequest = { showLogoutDialog = false }, + title = { Text("Logout") }, + text = { Text("Are you sure you want to logout?") }, + confirmButton = { + TextButton( + onClick = { + authViewModel.logout() + showLogoutDialog = false + } + ) { + Text( + text = "Logout", + color = MaterialTheme.colorScheme.error + ) + } + }, + dismissButton = { + TextButton(onClick = { showLogoutDialog = false }) { + Text("Cancel") + } + } + ) + } + } +} + +@Composable +private fun SettingsContent( + uiState: SettingsViewModel.SettingsUiState, + onToggleNotifications: (Boolean) -> Unit, + onToggleDarkMode: (Boolean) -> Unit, + onToggleBiometric: (Boolean) -> Unit, + onUpgradeSubscription: () -> Unit, + onShowLogoutDialog: () -> Unit, + modifier: Modifier = Modifier +) { + val user = uiState.user!! + + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { + AccountSection(user) + } + + item { + SubscriptionSection( + subscription = uiState.subscription, + onUpgrade = onUpgradeSubscription + ) + } + + item { + PreferencesSection( + notificationsEnabled = uiState.notificationsEnabled, + darkModeEnabled = uiState.darkModeEnabled, + biometricEnabled = uiState.biometricEnabled, + onToggleNotifications = onToggleNotifications, + onToggleDarkMode = onToggleDarkMode, + onToggleBiometric = onToggleBiometric + ) + } + + item { + Spacer(modifier = Modifier.height(16.dp)) + ShieldButton( + text = "Logout", + onClick = onShowLogoutDialog, + variant = ShieldButtonVariant.Danger, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +@Composable +private fun AccountSection(user: com.shieldai.android.data.model.User) { + Column { + Text( + text = "Account", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(12.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + ShieldAvatar( + name = user.name, + avatarUrl = user.avatarUrl + ) + Column { + Text( + text = user.name, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + Text( + text = user.email, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + if (user.emailVerified) { + ShieldBadge(text = "Email verified", variant = com.shieldai.android.ui.components.BadgeVariant.Success) + } + if (user.phoneVerified) { + ShieldBadge(text = "Phone verified", variant = com.shieldai.android.ui.components.BadgeVariant.Success) + } + } + } + } + } +} + +@Composable +private fun SubscriptionSection( + subscription: com.shieldai.android.data.model.Subscription?, + onUpgrade: () -> Unit +) { + Column { + Text( + text = "Subscription", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(8.dp)) + + ShieldCard { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = subscription?.plan ?: "Free", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium + ) + Text( + text = subscription?.status ?: "No subscription", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + ShieldButton( + text = "Upgrade", + onClick = onUpgrade, + variant = ShieldButtonVariant.Secondary, + size = com.shieldai.android.ui.components.ShieldButtonSize.Small + ) + } + } + } +} + +@Composable +private fun PreferencesSection( + notificationsEnabled: Boolean, + darkModeEnabled: Boolean, + biometricEnabled: Boolean, + onToggleNotifications: (Boolean) -> Unit, + onToggleDarkMode: (Boolean) -> Unit, + onToggleBiometric: (Boolean) -> Unit +) { + Column { + Text( + text = "Preferences", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(8.dp)) + + ShieldCard { + Column { + SettingRow( + title = "Notifications", + description = "Receive push notifications for alerts", + checked = notificationsEnabled, + onCheckedChange = onToggleNotifications + ) + Divider() + SettingRow( + title = "Dark Mode", + description = "Use dark theme", + checked = darkModeEnabled, + onCheckedChange = onToggleDarkMode + ) + Divider() + SettingRow( + title = "Biometric Auth", + description = "Use fingerprint or face unlock", + checked = biometricEnabled, + onCheckedChange = onToggleBiometric + ) + } + } + } +} + +@Composable +private fun SettingRow( + title: String, + description: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + .clickable { onCheckedChange(!checked) }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Switch( + checked = checked, + onCheckedChange = onCheckedChange + ) + } +} diff --git a/tasks/rebrand-to-kordant/README.md b/tasks/rebrand-to-kordant/README.md index a8a2d67..65093ba 100644 --- a/tasks/rebrand-to-kordant/README.md +++ b/tasks/rebrand-to-kordant/README.md @@ -8,9 +8,9 @@ Tasks - [x] 01 — Update monorepo foundation (root package, env, config) → `01-update-monorepo-foundation.md` - [x] 02 — Update database connection strings and DB names → `02-update-database-connection-strings.md` - [x] 03 — Update @shieldai/* package scopes and web imports → `03-update-web-package-scope-and-imports.md` -- [ ] 04 — Replace "ShieldAI" display text across web UI → `04-update-web-ui-brand-text.md` -- [ ] 05 — Update email templates, notification content, and queue names → `05-update-web-email-notification-templates.md` -- [ ] 06 — Update browser storage keys, theme keys, and user-agent headers → `06-update-web-storage-keys-and-constants.md` +- [x] 04 — Replace "ShieldAI" display text across web UI → `04-update-web-ui-brand-text.md` +- [x] 05 — Update email templates, notification content, and queue names → `05-update-web-email-notification-templates.md` +- [x] 06 — Update browser storage keys, theme keys, and user-agent headers → `06-update-web-storage-keys-and-constants.md` - [ ] 07 — Update seed data, blog content, and landing page copy → `07-update-web-seed-data-and-blog-content.md` - [ ] 08 — Update browser extension manifest, HTML, and storage keys → `08-update-browser-extension-branding.md` - [ ] 09 — Update iOS bundle ID, Xcode project, and Swift source branding → `09-update-ios-app-branding.md` diff --git a/todos.txt b/todos.txt new file mode 100644 index 0000000..7760409 --- /dev/null +++ b/todos.txt @@ -0,0 +1,8 @@ +- [ ] admin routes with appropriate controls and services dashboard +- [ ] make a /blog route that shows a chronological feed with featured one (if one is marked as such in db - should be available to manage in admin route +- [ ] create actual blogs filled with good advice to avoid scams and what to do when one has happened, ai detection advice etc. +- [ ] need pricing page, features/products page +- [ ] navbar should show links to main dashboard, and then specific products/features when logged in, so auth contextual +rendering +- [ ] apple logo svg is fucked up + diff --git a/web/src/app.tsx b/web/src/app.tsx index 4f73f74..6f1e692 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -64,7 +64,7 @@ function ClerkApp(props: { children: any }) { export default function App() { return ( - ShieldAI + Kordant