holy moly thats a lotta damage
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
|
||||
8
todos.txt
Normal file
8
todos.txt
Normal file
@@ -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
|
||||
|
||||
@@ -64,7 +64,7 @@ function ClerkApp(props: { children: any }) {
|
||||
export default function App() {
|
||||
return (
|
||||
<MetaProvider>
|
||||
<Title>ShieldAI</Title>
|
||||
<Title>Kordant</Title>
|
||||
<ThemeProvider>
|
||||
<ToastProvider>
|
||||
<Router
|
||||
|
||||
@@ -11,13 +11,13 @@ interface Testimonial {
|
||||
const testimonials: Testimonial[] = [
|
||||
{
|
||||
quote:
|
||||
"ShieldAI caught a credential leak before it became a disaster. Essential tool for anyone concerned about their digital identity.",
|
||||
"Kordant caught a credential leak before it became a disaster. Essential tool for anyone concerned about their digital identity.",
|
||||
author: "Sarah Chen",
|
||||
role: "Security Engineer",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"I sleep better knowing ShieldAI is monitoring my personal information 24/7.",
|
||||
"I sleep better knowing Kordant is monitoring my personal information 24/7.",
|
||||
author: "Marcus Johnson",
|
||||
role: "Freelance Developer",
|
||||
},
|
||||
@@ -52,7 +52,7 @@ export default function AuthLayout(props: AuthLayoutProps) {
|
||||
<div class="hidden md:flex flex-col justify-center gap-6 flex-1 p-8 lg:p-12">
|
||||
<div>
|
||||
<h1 class="text-3xl lg:text-4xl font-bold text-gradient-primary">
|
||||
ShieldAI
|
||||
Kordant
|
||||
</h1>
|
||||
<p class="text-lg text-[var(--color-text-secondary)] mt-2">
|
||||
AI-Powered Identity Protection
|
||||
|
||||
@@ -9,20 +9,32 @@ export default function SocialAuthButtons(props: SocialAuthButtonsProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onGoogleSignIn}
|
||||
class="flex items-center justify-center gap-3 w-full px-4 py-2.5 border border-[var(--color-border)] rounded-lg text-sm font-medium text-[var(--color-text-primary)] bg-white hover:bg-[var(--color-bg-secondary)] transition-colors cursor-pointer"
|
||||
class="flex items-center justify-center gap-3 w-full px-4 py-2.5 border border-(--color-border) rounded-lg text-sm font-medium text-black dark:text-white bg-white hover:bg-bg-secondary transition-colors cursor-pointer dark:bg-black"
|
||||
>
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4" />
|
||||
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
|
||||
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
|
||||
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
|
||||
<path
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
<path
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
fill="#34A853"
|
||||
/>
|
||||
<path
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
fill="#FBBC05"
|
||||
/>
|
||||
<path
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
fill="#EA4335"
|
||||
/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onAppleSignIn}
|
||||
class="flex items-center justify-center gap-3 w-full px-4 py-2.5 border border-[var(--color-border)] rounded-lg text-sm font-medium text-white bg-black hover:bg-gray-900 transition-colors cursor-pointer"
|
||||
class="flex items-center justify-center gap-3 w-full px-4 py-2.5 border border-(--color-border) rounded-lg text-sm font-medium text-white bg-black hover:bg-gray-900 transition-colors cursor-pointer"
|
||||
>
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.6 5.98.52 7.13-.62 1.28-1.4 2.55-2.57 3.08Zm-3.12-15.2c.03-1.14.44-2.23 1.07-3.03.82-.98 2.11-1.63 3.32-1.59.06 1.24-.4 2.45-1.12 3.3-.77.9-1.98 1.52-3.27 1.32Z" />
|
||||
|
||||
@@ -119,13 +119,13 @@ describe("AuthLayout", () => {
|
||||
expect(document.body.textContent).toContain("Form content");
|
||||
});
|
||||
|
||||
it("renders ShieldAI branding", () => {
|
||||
it("renders Kordant branding", () => {
|
||||
mount(() => (
|
||||
<AuthLayout>
|
||||
<p>Content</p>
|
||||
</AuthLayout>
|
||||
));
|
||||
expect(document.body.textContent).toContain("ShieldAI");
|
||||
expect(document.body.textContent).toContain("Kordant");
|
||||
});
|
||||
|
||||
it("renders gradient-card wrapper", () => {
|
||||
@@ -143,7 +143,7 @@ describe("AuthLayout", () => {
|
||||
<p>Content</p>
|
||||
</AuthLayout>
|
||||
));
|
||||
expect(document.body.textContent).toContain("ShieldAI");
|
||||
expect(document.body.textContent).toContain("Kordant");
|
||||
expect(document.body.textContent).toContain("AI-Powered Identity Protection");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,7 +109,7 @@ export default function Sidebar(props: SidebarProps) {
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="text-lg font-bold text-[var(--color-text-primary)]">ShieldAI</span>
|
||||
<span class="text-lg font-bold text-[var(--color-text-primary)]">Kordant</span>
|
||||
</A>
|
||||
</div>
|
||||
<nav class="p-4 space-y-1">
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function CTABannerSection(props: CTABannerSectionProps) {
|
||||
Ready to protect your identity?
|
||||
</h2>
|
||||
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto mb-10">
|
||||
Join thousands of users who trust ShieldAI to keep their digital
|
||||
Join thousands of users who trust Kordant to keep their digital
|
||||
identity safe from emerging threats.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function HeroSection(props: HeroSectionProps) {
|
||||
</h1>
|
||||
|
||||
<p class="text-xl md:text-2xl text-text-secondary max-w-2xl mb-10 leading-relaxed">
|
||||
Threat actors are using AI in multifaceted attacks. ShieldAI evens
|
||||
Threat actors are using AI in multifaceted attacks. Kordant evens
|
||||
the playing field using advanced AI to monitor, detect, and prevent
|
||||
identity threats in real-time.
|
||||
</p>
|
||||
|
||||
@@ -186,20 +186,20 @@ function ValueCard(props: ValueCardProps) {
|
||||
);
|
||||
}
|
||||
|
||||
interface WhyShieldAISectionProps {
|
||||
interface WhyKordantSectionProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export default function WhyShieldAISection(props: WhyShieldAISectionProps) {
|
||||
export default function WhyKordantSection(props: WhyKordantSectionProps) {
|
||||
return (
|
||||
<section
|
||||
id="why-shieldai"
|
||||
id="why-kordant"
|
||||
class={cn("py-20 md:py-28 scroll-mt-16", props.class)}
|
||||
>
|
||||
<PageContainer py="py-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
|
||||
Why ShieldAI
|
||||
Why Kordant
|
||||
</h2>
|
||||
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
|
||||
Built on cutting-edge technology with your privacy at the core
|
||||
@@ -40,7 +40,7 @@ describe("HeroSection", () => {
|
||||
|
||||
it("renders the subheadline", () => {
|
||||
mount(() => <HeroSection />);
|
||||
expect(document.body.textContent).toContain("ShieldAI uses advanced AI");
|
||||
expect(document.body.textContent).toContain("Kordant uses advanced AI");
|
||||
});
|
||||
|
||||
it("renders the Get Started CTA", () => {
|
||||
|
||||
@@ -3,5 +3,5 @@ export { default as HeroSection } from "./HeroSection";
|
||||
export { default as HowItWorksSection } from "./HowItWorksSection";
|
||||
export { default as FeaturesGridSection } from "./FeaturesGridSection";
|
||||
export { default as ForUsersSection } from "./ForUsersSection";
|
||||
export { default as WhyShieldAISection } from "./WhyShieldAISection";
|
||||
export { default as WhyKordantSection } from "./WhyKordantSection";
|
||||
export { default as CTABannerSection } from "./CTABannerSection";
|
||||
|
||||
@@ -16,7 +16,7 @@ vi.mock("@solidjs/router", () => ({
|
||||
import HowItWorksSection from "./HowItWorksSection";
|
||||
import FeaturesGridSection from "./FeaturesGridSection";
|
||||
import ForUsersSection from "./ForUsersSection";
|
||||
import WhyShieldAISection from "./WhyShieldAISection";
|
||||
import WhyKordantSection from "./WhyKordantSection";
|
||||
import CTABannerSection from "./CTABannerSection";
|
||||
|
||||
function mount(comp: () => JSX.Element): HTMLDivElement {
|
||||
@@ -226,28 +226,28 @@ describe("ForUsersSection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("WhyShieldAISection", () => {
|
||||
describe("WhyKordantSection", () => {
|
||||
it("renders the section heading", () => {
|
||||
mount(() => <WhyShieldAISection />);
|
||||
expect(document.body.textContent).toContain("Why ShieldAI");
|
||||
mount(() => <WhyKordantSection />);
|
||||
expect(document.body.textContent).toContain("Why Kordant");
|
||||
});
|
||||
|
||||
it("renders the section subheading", () => {
|
||||
mount(() => <WhyShieldAISection />);
|
||||
mount(() => <WhyKordantSection />);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Built on cutting-edge technology",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders all 3 value prop cards", () => {
|
||||
mount(() => <WhyShieldAISection />);
|
||||
mount(() => <WhyKordantSection />);
|
||||
expect(document.body.textContent).toContain("Proactive, Not Reactive");
|
||||
expect(document.body.textContent).toContain("AI-Powered Detection");
|
||||
expect(document.body.textContent).toContain("Privacy First");
|
||||
});
|
||||
|
||||
it("renders value prop descriptions", () => {
|
||||
mount(() => <WhyShieldAISection />);
|
||||
mount(() => <WhyKordantSection />);
|
||||
expect(document.body.textContent).toContain(
|
||||
"detect threats before they cause damage",
|
||||
);
|
||||
@@ -258,7 +258,7 @@ describe("WhyShieldAISection", () => {
|
||||
});
|
||||
|
||||
it("renders bullet items for each card", () => {
|
||||
mount(() => <WhyShieldAISection />);
|
||||
mount(() => <WhyKordantSection />);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Real-time dark web scanning",
|
||||
);
|
||||
@@ -269,25 +269,25 @@ describe("WhyShieldAISection", () => {
|
||||
});
|
||||
|
||||
it("renders 3 Card components", () => {
|
||||
mount(() => <WhyShieldAISection />);
|
||||
mount(() => <WhyKordantSection />);
|
||||
const cards = document.querySelectorAll(".gradient-card");
|
||||
expect(cards.length).toBe(3);
|
||||
});
|
||||
|
||||
it("has the anchor ID for smooth scrolling", () => {
|
||||
mount(() => <WhyShieldAISection />);
|
||||
const section = document.querySelector('#why-shieldai');
|
||||
mount(() => <WhyKordantSection />);
|
||||
const section = document.querySelector('#why-kordant');
|
||||
expect(section).toBeTruthy();
|
||||
});
|
||||
|
||||
it("applies custom class prop", () => {
|
||||
mount(() => <WhyShieldAISection class="custom-why" />);
|
||||
mount(() => <WhyKordantSection class="custom-why" />);
|
||||
const section = document.querySelector("section.custom-why");
|
||||
expect(section).toBeTruthy();
|
||||
});
|
||||
|
||||
it("uses three-column grid on desktop", () => {
|
||||
mount(() => <WhyShieldAISection />);
|
||||
mount(() => <WhyKordantSection />);
|
||||
const grid = document.querySelector(".grid-cols-1");
|
||||
expect(grid).toBeTruthy();
|
||||
expect(grid!.className).toContain("md:grid-cols-3");
|
||||
|
||||
@@ -10,7 +10,7 @@ interface AppShellProps {
|
||||
}
|
||||
|
||||
export default function AppShell(props: AppShellProps) {
|
||||
const title = () => props.title ?? "ShieldAI";
|
||||
const title = () => props.title ?? "Kordant";
|
||||
|
||||
onMount(() => {
|
||||
const onRouteChange = () => {
|
||||
|
||||
@@ -116,7 +116,7 @@ export default function Footer() {
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<ShieldLogo />
|
||||
<span class="text-lg font-bold text-[var(--color-text-primary)]">
|
||||
ShieldAI
|
||||
Kordant
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-[var(--color-text-secondary)] max-w-xs">
|
||||
@@ -169,7 +169,7 @@ export default function Footer() {
|
||||
|
||||
<div class="mt-12 pt-8 border-t border-[var(--color-border)] flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<p class="text-sm text-[var(--color-text-tertiary)]">
|
||||
{"\u00A9"} {new Date().getFullYear()} ShieldAI. All rights reserved.
|
||||
{"\u00A9"} {new Date().getFullYear()} Kordant. All rights reserved.
|
||||
</p>
|
||||
<div class="flex items-center gap-6">
|
||||
<A
|
||||
|
||||
@@ -196,7 +196,7 @@ export default function Navbar() {
|
||||
<A href="/" class="flex items-center gap-2">
|
||||
<ShieldLogo />
|
||||
<span class="text-lg font-bold text-[var(--color-text-primary)]">
|
||||
ShieldAI
|
||||
Kordant
|
||||
</span>
|
||||
</A>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export default createHandler(() => (
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<script
|
||||
innerHTML={`(function(){var t=localStorage.getItem('shieldai-theme');if(t==='light')return;if(t==='dark'){document.documentElement.classList.add('dark');return}if(window.matchMedia('(prefers-color-scheme:dark)').matches)document.documentElement.classList.add('dark')})()`}
|
||||
innerHTML={`(function(){var t=localStorage.getItem('kordant-theme');if(t==='light')return;if(t==='dark'){document.documentElement.classList.add('dark');return}if(window.matchMedia('(prefers-color-scheme:dark)').matches)document.documentElement.classList.add('dark')})()`}
|
||||
/>
|
||||
{assets}
|
||||
</head>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createSignal, createEffect, onMount, onCleanup } from "solid-js";
|
||||
import { createWebSocketClient, type AlertPayload, type ConnectionStatus } from "~/lib/websocket";
|
||||
import { useToast } from "~/components/ui";
|
||||
|
||||
const UNREAD_STORAGE_KEY = "shieldai_unread_count";
|
||||
const UNREAD_STORAGE_KEY = "kordant_unread_count";
|
||||
|
||||
function loadUnreadCount(): number {
|
||||
try {
|
||||
|
||||
@@ -71,17 +71,17 @@ describe("getStoredTheme", () => {
|
||||
});
|
||||
|
||||
it("returns 'light' when stored", () => {
|
||||
localStorage.setItem("shieldai-theme", "light");
|
||||
localStorage.setItem("kordant-theme", "light");
|
||||
expect(getStoredTheme()).toBe("light");
|
||||
});
|
||||
|
||||
it("returns 'dark' when stored", () => {
|
||||
localStorage.setItem("shieldai-theme", "dark");
|
||||
localStorage.setItem("kordant-theme", "dark");
|
||||
expect(getStoredTheme()).toBe("dark");
|
||||
});
|
||||
|
||||
it("returns 'system' for invalid value", () => {
|
||||
localStorage.setItem("shieldai-theme", "invalid");
|
||||
localStorage.setItem("kordant-theme", "invalid");
|
||||
expect(getStoredTheme()).toBe("system");
|
||||
});
|
||||
});
|
||||
@@ -159,12 +159,12 @@ describe("persistTheme", () => {
|
||||
|
||||
it("writes theme to localStorage", () => {
|
||||
persistTheme("dark");
|
||||
expect(localStorage.getItem("shieldai-theme")).toBe("dark");
|
||||
expect(localStorage.getItem("kordant-theme")).toBe("dark");
|
||||
});
|
||||
|
||||
it("writes 'system' to localStorage", () => {
|
||||
persistTheme("system");
|
||||
expect(localStorage.getItem("shieldai-theme")).toBe("system");
|
||||
expect(localStorage.getItem("kordant-theme")).toBe("system");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,7 +184,7 @@ describe("createThemeState", () => {
|
||||
|
||||
it("returns 'light' from localStorage", () => {
|
||||
vi.stubGlobal("matchMedia", createMatchMediaMock(false));
|
||||
localStorage.setItem("shieldai-theme", "light");
|
||||
localStorage.setItem("kordant-theme", "light");
|
||||
runWithRoot(() => {
|
||||
const { theme } = createThemeState();
|
||||
expect(theme()).toBe("light");
|
||||
@@ -193,7 +193,7 @@ describe("createThemeState", () => {
|
||||
|
||||
it("returns 'dark' from localStorage", () => {
|
||||
vi.stubGlobal("matchMedia", createMatchMediaMock(false));
|
||||
localStorage.setItem("shieldai-theme", "dark");
|
||||
localStorage.setItem("kordant-theme", "dark");
|
||||
runWithRoot(() => {
|
||||
const { theme } = createThemeState();
|
||||
expect(theme()).toBe("dark");
|
||||
@@ -226,7 +226,7 @@ describe("createThemeState", () => {
|
||||
const { setTheme, theme } = createThemeState();
|
||||
setTheme("dark");
|
||||
expect(theme()).toBe("dark");
|
||||
expect(localStorage.getItem("shieldai-theme")).toBe("dark");
|
||||
expect(localStorage.getItem("kordant-theme")).toBe("dark");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -235,7 +235,7 @@ describe("createThemeState", () => {
|
||||
runWithRoot(() => {
|
||||
const { setTheme } = createThemeState();
|
||||
setTheme("system");
|
||||
expect(localStorage.getItem("shieldai-theme")).toBe("system");
|
||||
expect(localStorage.getItem("kordant-theme")).toBe("system");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -243,7 +243,7 @@ describe("createThemeState", () => {
|
||||
describe("toggle", () => {
|
||||
it("toggles from light to dark", () => {
|
||||
vi.stubGlobal("matchMedia", createMatchMediaMock(false));
|
||||
localStorage.setItem("shieldai-theme", "light");
|
||||
localStorage.setItem("kordant-theme", "light");
|
||||
runWithRoot(() => {
|
||||
const { toggle, resolved, theme } = createThemeState();
|
||||
toggle();
|
||||
@@ -254,7 +254,7 @@ describe("createThemeState", () => {
|
||||
|
||||
it("toggles from dark to light", () => {
|
||||
vi.stubGlobal("matchMedia", createMatchMediaMock(true));
|
||||
localStorage.setItem("shieldai-theme", "dark");
|
||||
localStorage.setItem("kordant-theme", "dark");
|
||||
runWithRoot(() => {
|
||||
const { toggle, resolved, theme } = createThemeState();
|
||||
toggle();
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type Theme = "light" | "dark" | "system";
|
||||
type ResolvedTheme = "light" | "dark";
|
||||
|
||||
const STORAGE_KEY = "shieldai-theme";
|
||||
const STORAGE_KEY = "kordant-theme";
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Accessor<Theme>;
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function ForgotPasswordPage() {
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<Title>Forgot Password — ShieldAI</Title>
|
||||
<Title>Forgot Password — Kordant</Title>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Show
|
||||
when={!sent()}
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function LoginPage() {
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<Title>Sign In — ShieldAI</Title>
|
||||
<Title>Sign In — Kordant</Title>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-center">
|
||||
<h2 class="text-2xl font-bold text-[var(--color-text-primary)]">
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function OnboardingPage() {
|
||||
|
||||
return (
|
||||
<main class="min-h-screen flex flex-col items-center justify-center py-8 md:py-12 px-4">
|
||||
<Title>Set Up Your Account — ShieldAI</Title>
|
||||
<Title>Set Up Your Account — Kordant</Title>
|
||||
|
||||
<div class="w-full max-w-2xl">
|
||||
<div class="flex items-center justify-center gap-2 mb-8">
|
||||
@@ -388,7 +388,7 @@ export default function OnboardingPage() {
|
||||
You're all set!
|
||||
</h2>
|
||||
<p class="text-sm text-[var(--color-text-secondary)] mt-2 max-w-sm">
|
||||
Your ShieldAI account is ready. We're already monitoring your
|
||||
Your Kordant account is ready. We're already monitoring your
|
||||
selected items and will alert you of any threats.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function ResetPasswordPage() {
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<Title>Reset Password — ShieldAI</Title>
|
||||
<Title>Reset Password — Kordant</Title>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Show
|
||||
when={!success()}
|
||||
|
||||
@@ -2,7 +2,11 @@ import { createSignal, createMemo, Show } from "solid-js";
|
||||
import { Title } from "@solidjs/meta";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { useSignUp } from "clerk-solidjs";
|
||||
import { AuthLayout, PasswordInput, SocialAuthButtons } from "~/components/auth";
|
||||
import {
|
||||
AuthLayout,
|
||||
PasswordInput,
|
||||
SocialAuthButtons,
|
||||
} from "~/components/auth";
|
||||
import { Input } from "~/components/ui";
|
||||
import { Button } from "~/components/ui";
|
||||
|
||||
@@ -30,7 +34,11 @@ export default function SignupPage() {
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
const [serverError, setServerError] = createSignal("");
|
||||
|
||||
const strength = createMemo<{ level: StrengthLevel; label: string; percent: number }>(() => {
|
||||
const strength = createMemo<{
|
||||
level: StrengthLevel;
|
||||
label: string;
|
||||
percent: number;
|
||||
}>(() => {
|
||||
const pwd = password();
|
||||
if (!pwd) return { level: "none", label: "", percent: 0 };
|
||||
let score = 0;
|
||||
@@ -56,8 +64,10 @@ export default function SignupPage() {
|
||||
if (!email().trim()) errs.email = "Email is required";
|
||||
else if (!EMAIL_REGEX.test(email())) errs.email = "Invalid email format";
|
||||
if (!password()) errs.password = "Password is required";
|
||||
else if (password().length < 8) errs.password = "Password must be at least 8 characters";
|
||||
if (password() !== confirmPassword()) errs.confirmPassword = "Passwords do not match";
|
||||
else if (password().length < 8)
|
||||
errs.password = "Password must be at least 8 characters";
|
||||
if (password() !== confirmPassword())
|
||||
errs.confirmPassword = "Passwords do not match";
|
||||
if (!agreeTerms()) errs.terms = "You must agree to the Terms of Service";
|
||||
setErrors(errs);
|
||||
return Object.keys(errs).length === 0;
|
||||
@@ -80,7 +90,9 @@ export default function SignupPage() {
|
||||
await setActive({ session: result.createdSessionId });
|
||||
navigate("/onboarding", { replace: true });
|
||||
} else {
|
||||
setServerError("Additional verification is required. Please check your email.");
|
||||
setServerError(
|
||||
"Additional verification is required. Please check your email.",
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setServerError(
|
||||
@@ -107,20 +119,20 @@ export default function SignupPage() {
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<Title>Create Account — ShieldAI</Title>
|
||||
<Title>Create Account — Kordant</Title>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-center">
|
||||
<h2 class="text-2xl font-bold text-[var(--color-text-primary)]">
|
||||
<h2 class="text-2xl font-bold text-text-primary">
|
||||
Create your account
|
||||
</h2>
|
||||
<p class="text-sm text-[var(--color-text-secondary)] mt-1">
|
||||
<p class="text-sm text-text-secondary mt-1">
|
||||
Start protecting your identity
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Show when={serverError()}>
|
||||
<div
|
||||
class="px-4 py-3 rounded-lg text-sm bg-[var(--color-error-bg)] text-[var(--color-error)] border border-[var(--color-error)]/30"
|
||||
class="px-4 py-3 rounded-lg text-sm bg-error-bg text-(--color-error) border border-(--color-error)/30"
|
||||
role="alert"
|
||||
>
|
||||
{serverError()}
|
||||
@@ -160,21 +172,23 @@ export default function SignupPage() {
|
||||
/>
|
||||
<Show when={strength().level !== "none"}>
|
||||
<div class="mt-2">
|
||||
<div class="h-1.5 w-full bg-[var(--color-bg-tertiary)] rounded-full overflow-hidden">
|
||||
<div class="h-1.5 w-full bg-(--color-bg-tertiary) rounded-full overflow-hidden">
|
||||
<div
|
||||
class={`h-full rounded-full transition-all duration-300 ${strengthColors[strength().level]}`}
|
||||
class={`h-full rounded-full transition-all duration-300 ${
|
||||
strengthColors[strength().level]
|
||||
}`}
|
||||
style={{ width: `${strength().percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p class={`text-xs mt-1 text-[var(--color-text-tertiary)]`}>
|
||||
<p class={`text-xs mt-1 text-text-tertiary`}>
|
||||
Password strength:{" "}
|
||||
<span
|
||||
class={`font-medium ${
|
||||
strength().level === "weak"
|
||||
? "text-[var(--color-error)]"
|
||||
? "text-(--color-error)"
|
||||
: strength().level === "medium"
|
||||
? "text-[var(--color-warning)]"
|
||||
: "text-[var(--color-success)]"
|
||||
? "text-(--color-warning)"
|
||||
: "text-(--color-success)"
|
||||
}`}
|
||||
>
|
||||
{strength().label}
|
||||
@@ -192,26 +206,32 @@ export default function SignupPage() {
|
||||
error={errors().confirmPassword}
|
||||
required
|
||||
/>
|
||||
<label class="flex items-start gap-2 text-sm text-[var(--color-text-secondary)] cursor-pointer">
|
||||
<label class="flex items-start gap-2 text-sm text-text-secondary cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agreeTerms()}
|
||||
onChange={() => setAgreeTerms(!agreeTerms())}
|
||||
class="mt-0.5 rounded border-[var(--color-border)] text-[var(--color-brand-primary)] focus:ring-[var(--color-brand-primary)]"
|
||||
class="mt-0.5 rounded border-(--color-border) text-(--color-brand-primary) focus:ring-(--color-brand-primary)"
|
||||
/>
|
||||
<span>
|
||||
I agree to the{" "}
|
||||
<a href="/terms" class="text-[var(--color-brand-primary)] hover:underline">
|
||||
<a
|
||||
href="/terms"
|
||||
class="text-(--color-brand-primary) hover:underline"
|
||||
>
|
||||
Terms of Service
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a href="/privacy" class="text-[var(--color-brand-primary)] hover:underline">
|
||||
<a
|
||||
href="/privacy"
|
||||
class="text-(--color-brand-primary) hover:underline"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
</span>
|
||||
</label>
|
||||
<Show when={errors().terms}>
|
||||
<p class="text-sm text-[var(--color-error)] -mt-2">{errors().terms}</p>
|
||||
<p class="text-sm text-(--color-error) -mt-2">{errors().terms}</p>
|
||||
</Show>
|
||||
<Button type="submit" loading={loading()} class="w-full">
|
||||
Create Account
|
||||
@@ -220,10 +240,10 @@ export default function SignupPage() {
|
||||
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-[var(--color-border)]" />
|
||||
<div class="w-full border-t border-(--color-border)" />
|
||||
</div>
|
||||
<div class="relative flex justify-center text-xs uppercase">
|
||||
<span class="bg-[var(--gradient-card-start)] px-2 text-[var(--color-text-tertiary)]">
|
||||
<span class="bg-(--gradient-card-start) px-2 text-text-tertiary">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
@@ -234,7 +254,7 @@ export default function SignupPage() {
|
||||
onAppleSignIn={() => handleOAuth("oauth_apple")}
|
||||
/>
|
||||
|
||||
<p class="text-center text-sm text-[var(--color-text-secondary)]">
|
||||
<p class="text-center text-sm text-text-secondary">
|
||||
Already have an account?{" "}
|
||||
<a
|
||||
href="/login"
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function DarkWatchPage() {
|
||||
|
||||
return (
|
||||
<div class="flex h-[calc(100vh-4rem)] bg-[var(--color-bg)]">
|
||||
<Title>DarkWatch — ShieldAI</Title>
|
||||
<Title>DarkWatch — Kordant</Title>
|
||||
<Sidebar open={sidebarOpen()} onClose={() => setSidebarOpen(false)} />
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<TopBar onMenuToggle={() => setSidebarOpen(v => !v)} />
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function DashboardPage() {
|
||||
|
||||
return (
|
||||
<div class="flex h-[calc(100vh-4rem)] bg-[var(--color-bg)]">
|
||||
<Title>Dashboard — ShieldAI</Title>
|
||||
<Title>Dashboard — Kordant</Title>
|
||||
<Sidebar open={sidebarOpen()} onClose={() => setSidebarOpen(false)} />
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<TopBar onMenuToggle={() => setSidebarOpen(v => !v)} />
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function HomeTitlePage() {
|
||||
|
||||
return (
|
||||
<div class="flex h-[calc(100vh-4rem)] bg-[var(--color-bg)]">
|
||||
<Title>HomeTitle — ShieldAI</Title>
|
||||
<Title>HomeTitle — Kordant</Title>
|
||||
<Sidebar open={sidebarOpen()} onClose={() => setSidebarOpen(false)} />
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<TopBar onMenuToggle={() => setSidebarOpen(v => !v)} />
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function RemoveBrokersPage() {
|
||||
|
||||
return (
|
||||
<div class="flex h-[calc(100vh-4rem)] bg-[var(--color-bg)]">
|
||||
<Title>RemoveBrokers — ShieldAI</Title>
|
||||
<Title>RemoveBrokers — Kordant</Title>
|
||||
<Sidebar open={sidebarOpen()} onClose={() => setSidebarOpen(false)} />
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<TopBar onMenuToggle={() => setSidebarOpen(v => !v)} />
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function SettingsPage() {
|
||||
|
||||
return (
|
||||
<div class="flex h-[calc(100vh-4rem)] bg-[var(--color-bg)]">
|
||||
<Title>Settings — ShieldAI</Title>
|
||||
<Title>Settings — Kordant</Title>
|
||||
<Sidebar open={sidebarOpen()} onClose={() => setSidebarOpen(false)} />
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<TopBar onMenuToggle={() => setSidebarOpen(v => !v)} />
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function SpamShieldPage() {
|
||||
|
||||
return (
|
||||
<div class="flex h-[calc(100vh-4rem)] bg-[var(--color-bg)]">
|
||||
<Title>SpamShield — ShieldAI</Title>
|
||||
<Title>SpamShield — Kordant</Title>
|
||||
<Sidebar open={sidebarOpen()} onClose={() => setSidebarOpen(false)} />
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<TopBar onMenuToggle={() => setSidebarOpen(v => !v)} />
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function VoicePrintPage() {
|
||||
|
||||
return (
|
||||
<div class="flex h-[calc(100vh-4rem)] bg-[var(--color-bg)]">
|
||||
<Title>VoicePrint — ShieldAI</Title>
|
||||
<Title>VoicePrint — Kordant</Title>
|
||||
<Sidebar open={sidebarOpen()} onClose={() => setSidebarOpen(false)} />
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<TopBar onMenuToggle={() => setSidebarOpen(v => !v)} />
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Button } from "~/components/ui";
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main class="min-h-[60vh] flex items-center justify-center px-6">
|
||||
<Title>Not Found — ShieldAI</Title>
|
||||
<Title>Not Found — Kordant</Title>
|
||||
<HttpStatusCode code={404} />
|
||||
<div class="flex flex-col items-center text-center max-w-md gap-6">
|
||||
<svg
|
||||
|
||||
@@ -37,7 +37,7 @@ const plans = [
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
q: "How does ShieldAI detect voice clones?",
|
||||
q: "How does Kordant detect voice clones?",
|
||||
a: "VoicePrint analyzes over 200 acoustic features in real-time, including micro-tremors and breathing patterns that AI clones can't replicate accurately.",
|
||||
},
|
||||
{
|
||||
@@ -68,7 +68,7 @@ export default function AdsPage() {
|
||||
|
||||
return (
|
||||
<main>
|
||||
<Title>ShieldAI — Stop AI Scams Before They Reach You</Title>
|
||||
<Title>Kordant — Stop AI Scams Before They Reach You</Title>
|
||||
|
||||
<section class="relative py-20 md:py-28 overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-[var(--color-brand-primary)]/10 via-[var(--color-brand-accent)]/5 to-transparent" />
|
||||
@@ -80,7 +80,7 @@ export default function AdsPage() {
|
||||
<span class="text-gradient-primary">They Reach You</span>
|
||||
</h1>
|
||||
<p class="text-xl text-[var(--color-text-secondary)] mb-8 max-w-2xl mx-auto">
|
||||
ShieldAI uses advanced artificial intelligence to detect and block voice clones, dark web leaks, and identity threats in real-time.
|
||||
Kordant uses advanced artificial intelligence to detect and block voice clones, dark web leaks, and identity threats in real-time.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center mb-8">
|
||||
<A href={`/signup${searchParams.utm_source ? `?utm_source=${searchParams.utm_source}&utm_medium=${searchParams.utm_medium || ""}&utm_campaign=${searchParams.utm_campaign || ""}` : ""}`}>
|
||||
@@ -194,7 +194,7 @@ export default function AdsPage() {
|
||||
Trusted by Thousands
|
||||
</h2>
|
||||
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
|
||||
See what our users say about ShieldAI
|
||||
See what our users say about Kordant
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto">
|
||||
@@ -226,7 +226,7 @@ export default function AdsPage() {
|
||||
))}
|
||||
</div>
|
||||
<p class="text-sm text-[var(--color-text-secondary)] mb-4">
|
||||
"DarkWatch alerted me that my email was in a data breach within hours. ShieldAI saved me from a potential hack."
|
||||
"DarkWatch alerted me that my email was in a data breach within hours. Kordant saved me from a potential hack."
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-full bg-[var(--color-brand-primary)]/20 flex items-center justify-center text-xs font-bold text-[var(--color-brand-primary)]">MK</div>
|
||||
@@ -311,7 +311,7 @@ export default function AdsPage() {
|
||||
Ready to protect your identity?
|
||||
</h2>
|
||||
<p class="text-lg text-white/80 mb-8 max-w-2xl mx-auto">
|
||||
Join 50,000+ users who trust ShieldAI for AI-powered identity protection.
|
||||
Join 50,000+ users who trust Kordant for AI-powered identity protection.
|
||||
</p>
|
||||
<A href={`/signup${searchParams.utm_source ? `?utm_source=${searchParams.utm_source}&utm_medium=${searchParams.utm_medium || ""}&utm_campaign=${searchParams.utm_campaign || ""}` : ""}`}>
|
||||
<Button variant="primary" size="lg" class="bg-white text-[var(--color-brand-primary)] hover:bg-white/90 shadow-lg">
|
||||
|
||||
@@ -32,7 +32,7 @@ const blogPosts: BlogPost[] = [
|
||||
{
|
||||
slug: "dark-web-monitoring-guide",
|
||||
title: "The Complete Guide to Dark Web Monitoring",
|
||||
excerpt: "Learn how dark web monitoring works, what data gets exposed, and how ShieldAI keeps your information safe from cybercriminals.",
|
||||
excerpt: "Learn how dark web monitoring works, what data gets exposed, and how Kordant keeps your information safe from cybercriminals.",
|
||||
author: "Mike Reynolds",
|
||||
date: "May 10, 2026",
|
||||
readingTime: "8 min read",
|
||||
@@ -52,7 +52,7 @@ const blogPosts: BlogPost[] = [
|
||||
{
|
||||
slug: "deepfake-voice-scams",
|
||||
title: "Deepfake Voice Scams: How They Work and How to Spot Them",
|
||||
excerpt: "AI-generated voice clones are being used to impersonate loved ones. Here's what to listen for and how ShieldAI's VoicePrint can help.",
|
||||
excerpt: "AI-generated voice clones are being used to impersonate loved ones. Here's what to listen for and how Kordant's VoicePrint can help.",
|
||||
author: "Sarah Chen",
|
||||
date: "April 28, 2026",
|
||||
readingTime: "7 min read",
|
||||
@@ -71,7 +71,7 @@ const blogPosts: BlogPost[] = [
|
||||
},
|
||||
{
|
||||
slug: "shieldai-product-update-may-2026",
|
||||
title: "ShieldAI Product Update — May 2026",
|
||||
title: "Kordant Product Update — May 2026",
|
||||
excerpt: "New features including improved VoicePrint detection, expanded dark web monitoring, and a redesigned dashboard experience.",
|
||||
author: "Product Team",
|
||||
date: "April 15, 2026",
|
||||
@@ -98,14 +98,14 @@ export default function BlogPage() {
|
||||
|
||||
return (
|
||||
<main>
|
||||
<Title>ShieldAI Blog — AI-Powered Identity Protection</Title>
|
||||
<Title>Kordant Blog — AI-Powered Identity Protection</Title>
|
||||
|
||||
<section class="relative py-20 md:py-28 overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-[var(--color-brand-primary)]/5 to-transparent" />
|
||||
<PageContainer class="relative z-10">
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-text-primary)] mb-4">
|
||||
ShieldAI Blog
|
||||
Kordant Blog
|
||||
</h1>
|
||||
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
|
||||
Insights on identity protection, AI safety, and the latest digital threats
|
||||
|
||||
@@ -43,12 +43,12 @@ AI-generated phishing emails are now nearly indistinguishable from legitimate co
|
||||
|
||||
1. **Verify voice requests** — If someone calls asking for money or sensitive information, hang up and call them back on a trusted number.
|
||||
2. **Use a safe word** — Establish a family safe word that can be used to verify identity in suspicious situations.
|
||||
3. **Enable ShieldAI VoicePrint** — Our AI detects voice clones by analyzing acoustic fingerprints that deepfakes cannot replicate.
|
||||
3. **Enable Kordant VoicePrint** — Our AI detects voice clones by analyzing acoustic fingerprints that deepfakes cannot replicate.
|
||||
4. **Stay skeptical of urgency** — Scammers create false urgency to bypass your critical thinking.
|
||||
|
||||
## The ShieldAI Advantage
|
||||
## The Kordant Advantage
|
||||
|
||||
ShieldAI's multi-layered protection uses machine learning models trained on millions of scam attempts to identify emerging threats before they reach you. Our DarkWatch service continuously scans the dark web for exposed credentials, while VoicePrint protects against audio deepfakes.`,
|
||||
Kordant's multi-layered protection uses machine learning models trained on millions of scam attempts to identify emerging threats before they reach you. Our DarkWatch service continuously scans the dark web for exposed credentials, while VoicePrint protects against audio deepfakes.`,
|
||||
author: "Sarah Chen",
|
||||
authorRole: "Security Researcher",
|
||||
date: "May 15, 2026",
|
||||
@@ -59,7 +59,7 @@ ShieldAI's multi-layered protection uses machine learning models trained on mill
|
||||
{
|
||||
slug: "dark-web-monitoring-guide",
|
||||
title: "The Complete Guide to Dark Web Monitoring",
|
||||
excerpt: "Learn how dark web monitoring works and how ShieldAI keeps your information safe.",
|
||||
excerpt: "Learn how dark web monitoring works and how Kordant keeps your information safe.",
|
||||
content: `## What Is Dark Web Monitoring?
|
||||
|
||||
The dark web is a hidden part of the internet where cybercriminals trade stolen data. Dark web monitoring services scan these hidden marketplaces, forums, and chat channels for your personal information.
|
||||
@@ -136,7 +136,7 @@ Your data broker profile can include your home address, phone number, email addr
|
||||
|
||||
### How RemoveBrokers Helps
|
||||
|
||||
ShieldAI's RemoveBrokers service automates the opt-out process for hundreds of data broker sites, sending removal requests on your behalf and verifying that your information has been deleted.`,
|
||||
Kordant's RemoveBrokers service automates the opt-out process for hundreds of data broker sites, sending removal requests on your behalf and verifying that your information has been deleted.`,
|
||||
author: "Alex Kim",
|
||||
authorRole: "Data Privacy Specialist",
|
||||
date: "April 20, 2026",
|
||||
@@ -146,9 +146,9 @@ ShieldAI's RemoveBrokers service automates the opt-out process for hundreds of d
|
||||
},
|
||||
{
|
||||
slug: "shieldai-product-update-may-2026",
|
||||
title: "ShieldAI Product Update — May 2026",
|
||||
title: "Kordant Product Update — May 2026",
|
||||
excerpt: "New features including improved VoicePrint detection and redesigned dashboard.",
|
||||
content: `## What's New in ShieldAI
|
||||
content: `## What's New in Kordant
|
||||
|
||||
We're excited to announce our May 2026 product update, packed with new features and improvements based on your feedback.
|
||||
|
||||
@@ -164,7 +164,7 @@ The dashboard has been completely redesigned for faster access to critical infor
|
||||
|
||||
We've added monitoring for 50 additional dark web forums and marketplaces, bringing our total coverage to over 200 sources.`,
|
||||
author: "Product Team",
|
||||
authorRole: "ShieldAI",
|
||||
authorRole: "Kordant",
|
||||
date: "April 15, 2026",
|
||||
readingTime: "3 min read",
|
||||
coverImage: "",
|
||||
@@ -229,7 +229,7 @@ export default function BlogPostPage() {
|
||||
when={post()}
|
||||
fallback={
|
||||
<main class="py-20 text-center">
|
||||
<Title>Post Not Found — ShieldAI</Title>
|
||||
<Title>Post Not Found — Kordant</Title>
|
||||
<PageContainer>
|
||||
<div class="max-w-md mx-auto">
|
||||
<h1 class="text-2xl font-bold text-[var(--color-text-primary)] mb-3">Post Not Found</h1>
|
||||
@@ -243,7 +243,7 @@ export default function BlogPostPage() {
|
||||
}
|
||||
>
|
||||
<main>
|
||||
<Title>{post()!.title} — ShieldAI Blog</Title>
|
||||
<Title>{post()!.title} — Kordant Blog</Title>
|
||||
|
||||
<article>
|
||||
<section class="relative py-16 md:py-20 overflow-hidden">
|
||||
|
||||
@@ -5,14 +5,14 @@ import {
|
||||
HowItWorksSection,
|
||||
FeaturesGridSection,
|
||||
ForUsersSection,
|
||||
WhyShieldAISection,
|
||||
WhyKordantSection,
|
||||
CTABannerSection,
|
||||
} from "~/components/landing";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main class="overflow-hidden" style="--cut: clamp(16px, 2.5vw, 40px)">
|
||||
<Title>ShieldAI — AI-Powered Identity Protection</Title>
|
||||
<Title>Kordant — AI-Powered Identity Protection</Title>
|
||||
|
||||
<ColorWaveBackground yOffset={-0.1} scale={0.65} speed={0.5} />
|
||||
<div class="relative z-10">
|
||||
@@ -53,7 +53,7 @@ export default function Home() {
|
||||
"clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)",
|
||||
}}
|
||||
>
|
||||
<WhyShieldAISection />
|
||||
<WhyKordantSection />
|
||||
<CTABannerSection />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -100,7 +100,7 @@ afterEach(() => {
|
||||
describe("BlogPage (listing)", () => {
|
||||
it("renders hero section with blog headline", () => {
|
||||
mount(() => <BlogPage />);
|
||||
expect(document.body.textContent).toContain("ShieldAI Blog");
|
||||
expect(document.body.textContent).toContain("Kordant Blog");
|
||||
});
|
||||
|
||||
it("renders all 6 blog post cards", () => {
|
||||
@@ -199,7 +199,7 @@ describe("AdsPage", () => {
|
||||
it("renders FAQ section with toggle functionality", () => {
|
||||
mount(() => <AdsPage />);
|
||||
expect(document.body.textContent).toContain("Frequently Asked Questions");
|
||||
expect(document.body.textContent).toContain("How does ShieldAI detect voice clones?");
|
||||
expect(document.body.textContent).toContain("How does Kordant detect voice clones?");
|
||||
expect(document.body.textContent).toContain("Is my data encrypted?");
|
||||
});
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export const extensionRouter = createTRPCRouter({
|
||||
deviceType: "desktop",
|
||||
platform: "web",
|
||||
token: input.extensionId,
|
||||
appName: input.deviceName ?? "ShieldAI Browser Extension",
|
||||
appName: input.deviceName ?? "Kordant Browser Extension",
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ export async function scanHIBP(email: string): Promise<ScanResult[]> {
|
||||
const res = await fetchWithCircuit(
|
||||
"hibp",
|
||||
`https://haveibeenpwned.com/api/v3/breachedaccount/${encodeURIComponent(email)}?truncateResponse=false`,
|
||||
{ "hibp-api-key": apiKey, "user-agent": "ShieldAI-DarkWatch" },
|
||||
{ "hibp-api-key": apiKey, "user-agent": "Kordant-DarkWatch" },
|
||||
);
|
||||
if (!res) return [];
|
||||
const breaches = await res.json() as Array<{ Name: string; BreachDate: string; DataClasses: string[]; Description: string }>;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{title}} — ShieldAI Annual Report</title>
|
||||
<title>{{title}} — Kordant Annual Report</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #1a73e8;
|
||||
@@ -54,7 +54,7 @@
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="logo">Shield<span>AI</span></div>
|
||||
<div class="logo">Kordant</div>
|
||||
<h1>{{title}}</h1>
|
||||
<div class="subtitle">{{periodStart}} — {{periodEnd}} | Annual Comprehensive Security Report</div>
|
||||
</div>
|
||||
@@ -107,7 +107,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Generated by ShieldAI on {{generatedAt}}</p>
|
||||
<p>Generated by Kordant on {{generatedAt}}</p>
|
||||
<p>This report contains sensitive security information. Please keep it confidential.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{title}} — ShieldAI Monthly Report</title>
|
||||
<title>{{title}} — Kordant Monthly Report</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #1a73e8;
|
||||
@@ -51,7 +51,7 @@
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="logo">Shield<span>AI</span></div>
|
||||
<div class="logo">Kordant</div>
|
||||
<h1>{{title}}</h1>
|
||||
<div class="subtitle">{{periodStart}} — {{periodEnd}} | Monthly Security Summary</div>
|
||||
</div>
|
||||
@@ -94,7 +94,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Generated by ShieldAI on {{generatedAt}}</p>
|
||||
<p>Generated by Kordant on {{generatedAt}}</p>
|
||||
<p>This report contains sensitive security information. Please keep it confidential.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{title}} — ShieldAI Weekly Digest</title>
|
||||
<title>{{title}} — Kordant Weekly Digest</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #1a73e8;
|
||||
@@ -36,7 +36,7 @@
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="logo">Shield<span>AI</span></div>
|
||||
<div class="logo">Kordant</div>
|
||||
<h1>Weekly Security Digest</h1>
|
||||
<div class="subtitle">{{periodStart}} — {{periodEnd}}</div>
|
||||
</div>
|
||||
@@ -56,7 +56,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Generated by ShieldAI on {{generatedAt}}</p>
|
||||
<p>Generated by Kordant on {{generatedAt}}</p>
|
||||
<p>This digest contains sensitive security information. Please keep it confidential.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user