holy moly thats a lotta damage

This commit is contained in:
2026-05-25 22:10:19 -04:00
parent c01c1a5636
commit b62ab77fbe
47 changed files with 1444 additions and 129 deletions

View File

@@ -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
)
}
}
}
}

View File

@@ -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
)
}
}
}
}

View File

@@ -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
)
}
}
}
}

View File

@@ -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
)
}
}

View File

@@ -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
View 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

View File

@@ -64,7 +64,7 @@ function ClerkApp(props: { children: any }) {
export default function App() {
return (
<MetaProvider>
<Title>ShieldAI</Title>
<Title>Kordant</Title>
<ThemeProvider>
<ToastProvider>
<Router

View File

@@ -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

View File

@@ -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" />

View File

@@ -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");
});
});

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>

View File

@@ -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

View File

@@ -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", () => {

View File

@@ -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";

View File

@@ -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");

View File

@@ -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 = () => {

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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>;

View File

@@ -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()}

View File

@@ -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)]">

View File

@@ -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>

View File

@@ -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()}

View File

@@ -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"

View File

@@ -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)} />

View File

@@ -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)} />

View File

@@ -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)} />

View File

@@ -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)} />

View File

@@ -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)} />

View File

@@ -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)} />

View File

@@ -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)} />

View File

@@ -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

View File

@@ -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">

View File

@@ -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

View File

@@ -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">

View File

@@ -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>

View File

@@ -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?");
});

View File

@@ -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();

View File

@@ -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 }>;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>