Refactor iOS and Android views into separate files

Organized view components by extracting composables and views into separate files following single responsibility principle.

iOS Changes:
- Split MainTabView → extracted ProfileTabView
- Split CompleteTaskView → extracted ImageThumbnailView, CameraPickerView
- Split ManageUsersView → extracted ShareCodeCard, UserListItem
- Consolidated task action buttons into single TaskActionButtons.swift file
- Split HomeScreenView → extracted OverviewCard, StatView, HomeNavigationCard
- Split AllTasksView → extracted DynamicTaskColumnView, DynamicTaskCard
- Split ContentView → extracted ComposeView, CustomView

Android Changes:
- Split ResetPasswordScreen → extracted RequirementItem component
- Split TasksScreen → extracted TaskPill component
- Created TaskDisplayUtils for shared helper functions (getIconFromName, hexToColor)

All extracted components properly organized in:
- iOS: Subviews/Common, Subviews/Task, Subviews/Residence, Profile
- Android: ui/components/auth, ui/components/task, ui/utils

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-10 11:38:17 -06:00
parent 29b4c99f08
commit 77a118a6f7
23 changed files with 780 additions and 747 deletions

View File

@@ -0,0 +1,36 @@
package com.mycrib.android.ui.components.auth
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Circle
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun RequirementItem(text: String, satisfied: Boolean) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
if (satisfied) Icons.Default.CheckCircle else Icons.Default.Circle,
contentDescription = null,
tint = if (satisfied) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text,
style = MaterialTheme.typography.bodySmall,
color = if (satisfied) MaterialTheme.colorScheme.onSecondaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
)
}
}

View File

@@ -0,0 +1,42 @@
package com.mycrib.android.ui.components.task
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun TaskPill(
count: Int,
label: String,
color: Color
) {
Surface(
color = color.copy(alpha = 0.1f),
shape = MaterialTheme.shapes.small
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = count.toString(),
style = MaterialTheme.typography.labelLarge,
color = color
)
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = color
)
}
}
}

View File

@@ -15,6 +15,7 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.mycrib.android.ui.components.auth.AuthHeader
import com.mycrib.android.ui.components.auth.RequirementItem
import com.mycrib.android.ui.components.common.ErrorCard
import com.mycrib.android.viewmodel.PasswordResetViewModel
import com.mycrib.shared.network.ApiResult
@@ -254,23 +255,3 @@ fun ResetPasswordScreen(
}
}
}
@Composable
private fun RequirementItem(text: String, satisfied: Boolean) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
if (satisfied) Icons.Default.CheckCircle else Icons.Default.Circle,
contentDescription = null,
tint = if (satisfied) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text,
style = MaterialTheme.typography.bodySmall,
color = if (satisfied) MaterialTheme.colorScheme.onSecondaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
)
}
}

View File

@@ -12,6 +12,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.ui.components.CompleteTaskDialog
import com.mycrib.android.ui.components.task.TaskCard
import com.mycrib.android.ui.components.task.TaskPill
import com.mycrib.android.ui.utils.getIconFromName
import com.mycrib.android.ui.utils.hexToColor
import com.mycrib.android.viewmodel.TaskCompletionViewModel
import com.mycrib.android.viewmodel.TaskViewModel
import com.mycrib.shared.network.ApiResult
@@ -271,108 +274,3 @@ fun TasksScreen(
)
}
}
@Composable
private fun TaskPill(
count: Int,
label: String,
color: androidx.compose.ui.graphics.Color
) {
Surface(
color = color.copy(alpha = 0.1f),
shape = MaterialTheme.shapes.small
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
Text(
text = count.toString(),
style = MaterialTheme.typography.labelLarge,
color = color
)
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = color
)
}
}
}
@Composable
private fun getColumnColor(columnName: String): androidx.compose.ui.graphics.Color {
return when (columnName) {
"upcoming_tasks" -> MaterialTheme.colorScheme.primary
"in_progress_tasks" -> MaterialTheme.colorScheme.tertiary
"done_tasks" -> MaterialTheme.colorScheme.secondary
"archived_tasks" -> MaterialTheme.colorScheme.outline
else -> MaterialTheme.colorScheme.primary // Default color for unknown columns
}
}
@Composable
private fun getColumnIcon(columnName: String): androidx.compose.ui.graphics.vector.ImageVector {
return when (columnName) {
"upcoming_tasks" -> Icons.Default.CalendarToday
"in_progress_tasks" -> Icons.Default.PlayArrow
"done_tasks" -> Icons.Default.CheckCircle
"archived_tasks" -> Icons.Default.Archive
else -> Icons.Default.List // Default icon for unknown columns
}
}
/**
* Helper function to convert icon name string to ImageVector
*/
private fun getIconFromName(iconName: String): androidx.compose.ui.graphics.vector.ImageVector {
return when (iconName) {
"CalendarToday" -> Icons.Default.CalendarToday
"PlayCircle" -> Icons.Default.PlayCircle
"PlayArrow" -> Icons.Default.PlayArrow
"CheckCircle" -> Icons.Default.CheckCircle
"Archive" -> Icons.Default.Archive
"List" -> Icons.Default.List
"Unarchive" -> Icons.Default.Unarchive
else -> Icons.Default.List // Default fallback
}
}
/**
* Helper function to convert hex color string to Color
* Supports formats: #RGB, #RRGGBB, #AARRGGBB
* Platform-independent implementation
*/
private fun hexToColor(hex: String): androidx.compose.ui.graphics.Color {
val cleanHex = hex.removePrefix("#")
return try {
when (cleanHex.length) {
3 -> {
// RGB format - expand to RRGGBB
val r = cleanHex[0].toString().repeat(2).toInt(16)
val g = cleanHex[1].toString().repeat(2).toInt(16)
val b = cleanHex[2].toString().repeat(2).toInt(16)
androidx.compose.ui.graphics.Color(red = r / 255f, green = g / 255f, blue = b / 255f)
}
6 -> {
// RRGGBB format
val r = cleanHex.substring(0, 2).toInt(16)
val g = cleanHex.substring(2, 4).toInt(16)
val b = cleanHex.substring(4, 6).toInt(16)
androidx.compose.ui.graphics.Color(red = r / 255f, green = g / 255f, blue = b / 255f)
}
8 -> {
// AARRGGBB format
val a = cleanHex.substring(0, 2).toInt(16)
val r = cleanHex.substring(2, 4).toInt(16)
val g = cleanHex.substring(4, 6).toInt(16)
val b = cleanHex.substring(6, 8).toInt(16)
androidx.compose.ui.graphics.Color(red = r / 255f, green = g / 255f, blue = b / 255f, alpha = a / 255f)
}
else -> androidx.compose.ui.graphics.Color.Gray // Default fallback
}
} catch (e: Exception) {
androidx.compose.ui.graphics.Color.Gray // Fallback on parse error
}
}

View File

@@ -0,0 +1,60 @@
package com.mycrib.android.ui.utils
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
/**
* Helper function to convert icon name string to ImageVector
*/
fun getIconFromName(iconName: String): ImageVector {
return when (iconName) {
"CalendarToday" -> Icons.Default.CalendarToday
"PlayCircle" -> Icons.Default.PlayCircle
"PlayArrow" -> Icons.Default.PlayArrow
"CheckCircle" -> Icons.Default.CheckCircle
"Archive" -> Icons.Default.Archive
"List" -> Icons.Default.List
"Unarchive" -> Icons.Default.Unarchive
else -> Icons.Default.List // Default fallback
}
}
/**
* Helper function to convert hex color string to Color
* Supports formats: #RGB, #RRGGBB, #AARRGGBB
* Platform-independent implementation
*/
fun hexToColor(hex: String): Color {
val cleanHex = hex.removePrefix("#")
return try {
when (cleanHex.length) {
3 -> {
// RGB format - expand to RRGGBB
val r = cleanHex[0].toString().repeat(2).toInt(16)
val g = cleanHex[1].toString().repeat(2).toInt(16)
val b = cleanHex[2].toString().repeat(2).toInt(16)
Color(red = r / 255f, green = g / 255f, blue = b / 255f)
}
6 -> {
// RRGGBB format
val r = cleanHex.substring(0, 2).toInt(16)
val g = cleanHex.substring(2, 4).toInt(16)
val b = cleanHex.substring(4, 6).toInt(16)
Color(red = r / 255f, green = g / 255f, blue = b / 255f)
}
8 -> {
// AARRGGBB format
val a = cleanHex.substring(0, 2).toInt(16)
val r = cleanHex.substring(2, 4).toInt(16)
val g = cleanHex.substring(4, 6).toInt(16)
val b = cleanHex.substring(6, 8).toInt(16)
Color(red = r / 255f, green = g / 255f, blue = b / 255f, alpha = a / 255f)
}
else -> Color.Gray // Default fallback
}
} catch (e: Exception) {
Color.Gray // Fallback on parse error
}
}

View File

@@ -1,46 +1,9 @@
import UIKit
import SwiftUI
import ComposeApp
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
struct ContentView: View {
var body: some View {
CustomView()
.ignoresSafeArea()
}
}
struct CustomView: View {
var body: some View {
Text("Custom view")
.task {
await ViewModel().somethingRandom()
}
}
}
class ViewModel {
func somethingRandom() async {
TokenStorage().initialize(manager: TokenManager.init())
// TokenStorage.initialize(TokenManager.getInstance())
let api = ResidenceApi(client: ApiClient_iosKt.createHttpClient())
api.deleteResidence(token: "token", id: 32) { result, error in
if let error = error {
print("Interop error: \(error)")
return
}
guard let result = result else { return }
}
}
}

View File

@@ -62,104 +62,6 @@ struct HomeScreenView: View {
}
}
struct OverviewCard: View {
let summary: OverallSummary
var body: some View {
VStack(spacing: 16) {
HStack {
Image(systemName: "chart.bar.fill")
.font(.title3)
Text("Overview")
.font(.title2)
.fontWeight(.bold)
Spacer()
}
HStack(spacing: 40) {
StatView(
icon: "house.fill",
value: "\(summary.totalResidences)",
label: "Properties"
)
StatView(
icon: "list.bullet",
value: "\(summary.totalTasks)",
label: "Total Tasks"
)
StatView(
icon: "clock.fill",
value: "\(summary.totalPending)",
label: "Pending"
)
}
}
.padding(20)
.background(Color.blue.opacity(0.1))
.cornerRadius(16)
.padding(.horizontal)
}
}
struct StatView: View {
let icon: String
let value: String
let label: String
var body: some View {
VStack(spacing: 8) {
Image(systemName: icon)
.font(.title2)
.foregroundColor(.blue)
Text(value)
.font(.title)
.fontWeight(.bold)
Text(label)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
struct HomeNavigationCard: View {
let icon: String
let title: String
let subtitle: String
var body: some View {
HStack(spacing: 16) {
Image(systemName: icon)
.font(.system(size: 36))
.foregroundColor(.blue)
.frame(width: 60)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.title3)
.fontWeight(.semibold)
.foregroundColor(.primary)
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
}
.padding(20)
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
}
}
#Preview {
HomeScreenView()
}

View File

@@ -33,76 +33,6 @@ struct MainTabView: View {
}
}
struct ProfileTabView: View {
@EnvironmentObject var loginViewModel: LoginViewModel
@State private var showingProfileEdit = false
var body: some View {
List {
Section {
HStack {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 60, height: 60)
.foregroundColor(.blue)
VStack(alignment: .leading, spacing: 4) {
Text("User Profile")
.font(.headline)
Text("Manage your account")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 8)
}
Section("Account") {
Button(action: {
showingProfileEdit = true
}) {
Label("Edit Profile", systemImage: "person.crop.circle")
.foregroundColor(.primary)
}
NavigationLink(destination: Text("Notifications")) {
Label("Notifications", systemImage: "bell")
}
NavigationLink(destination: Text("Privacy")) {
Label("Privacy", systemImage: "lock.shield")
}
}
Section {
Button(action: {
loginViewModel.logout()
}) {
Label("Log Out", systemImage: "rectangle.portrait.and.arrow.right")
.foregroundColor(.red)
}
}
Section {
VStack(alignment: .leading, spacing: 4) {
Text("MyCrib")
.font(.caption)
.fontWeight(.semibold)
Text("Version 1.0.0")
.font(.caption2)
.foregroundColor(.secondary)
}
}
}
.navigationTitle("Profile")
.sheet(isPresented: $showingProfileEdit) {
ProfileView()
}
}
}
#Preview {
MainTabView()
}

View File

@@ -0,0 +1,71 @@
import SwiftUI
struct ProfileTabView: View {
@EnvironmentObject var loginViewModel: LoginViewModel
@State private var showingProfileEdit = false
var body: some View {
List {
Section {
HStack {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 60, height: 60)
.foregroundColor(.blue)
VStack(alignment: .leading, spacing: 4) {
Text("User Profile")
.font(.headline)
Text("Manage your account")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 8)
}
Section("Account") {
Button(action: {
showingProfileEdit = true
}) {
Label("Edit Profile", systemImage: "person.crop.circle")
.foregroundColor(.primary)
}
NavigationLink(destination: Text("Notifications")) {
Label("Notifications", systemImage: "bell")
}
NavigationLink(destination: Text("Privacy")) {
Label("Privacy", systemImage: "lock.shield")
}
}
Section {
Button(action: {
loginViewModel.logout()
}) {
Label("Log Out", systemImage: "rectangle.portrait.and.arrow.right")
.foregroundColor(.red)
}
}
Section {
VStack(alignment: .leading, spacing: 4) {
Text("MyCrib")
.font(.caption)
.fontWeight(.semibold)
Text("Version 1.0.0")
.font(.caption2)
.foregroundColor(.secondary)
}
}
}
.navigationTitle("Profile")
.sheet(isPresented: $showingProfileEdit) {
ProfileView()
}
}
}

View File

@@ -155,118 +155,6 @@ struct ManageUsersView: View {
}
}
// MARK: - Share Code Card
struct ShareCodeCard: View {
let shareCode: ResidenceShareCode?
let residenceName: String
let isGeneratingCode: Bool
let onGenerateCode: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("Share Code")
.font(.subheadline)
.foregroundColor(.secondary)
if let shareCode = shareCode {
Text(shareCode.code)
.font(.title)
.fontWeight(.bold)
.foregroundColor(.blue)
} else {
Text("No active code")
.font(.body)
.foregroundColor(.secondary)
}
}
Spacer()
Button(action: onGenerateCode) {
HStack {
Image(systemName: "square.and.arrow.up")
Text(shareCode != nil ? "New Code" : "Generate")
}
}
.buttonStyle(.borderedProminent)
.disabled(isGeneratingCode)
}
if shareCode != nil {
Text("Share this code with others to give them access to \(residenceName)")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding()
.background(Color.blue.opacity(0.1))
.cornerRadius(12)
}
}
// MARK: - User List Item
struct UserListItem: View {
let user: ResidenceUser
let isOwner: Bool
let isPrimaryOwner: Bool
let onRemove: () -> Void
var body: some View {
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(user.username)
.font(.body)
.fontWeight(.medium)
if isOwner {
Text("Owner")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.blue)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.blue.opacity(0.1))
.cornerRadius(4)
}
}
if !user.email.isEmpty {
Text(user.email)
.font(.caption)
.foregroundColor(.secondary)
}
let fullName = [user.firstName, user.lastName]
.compactMap { $0 }
.filter { !$0.isEmpty }
.joined(separator: " ")
if !fullName.isEmpty {
Text(fullName)
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
if isPrimaryOwner && !isOwner {
Button(action: onRemove) {
Image(systemName: "trash")
.foregroundColor(.red)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.padding(.horizontal)
}
}
#Preview {
ManageUsersView(residenceId: 1, residenceName: "My Home", isPrimaryOwner: true)
}

View File

@@ -0,0 +1,38 @@
import SwiftUI
struct CameraPickerView: UIViewControllerRepresentable {
@Environment(\.dismiss) var dismiss
let onImageCaptured: (UIImage) -> Void
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: CameraPickerView
init(_ parent: CameraPickerView) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[.originalImage] as? UIImage {
parent.onImageCaptured(image)
}
parent.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parent.dismiss()
}
}
}

View File

@@ -0,0 +1,11 @@
import UIKit
import SwiftUI
import ComposeApp
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

View File

@@ -0,0 +1,28 @@
import SwiftUI
import ComposeApp
struct CustomView: View {
var body: some View {
Text("Custom view")
.task {
await ViewModel().somethingRandom()
}
}
}
class ViewModel {
func somethingRandom() async {
TokenStorage().initialize(manager: TokenManager.init())
// TokenStorage.initialize(TokenManager.getInstance())
let api = ResidenceApi(client: ApiClient_iosKt.createHttpClient())
api.deleteResidence(token: "token", id: 32) { result, error in
if let error = error {
print("Interop error: \(error)")
return
}
guard let result = result else { return }
}
}
}

View File

@@ -0,0 +1,36 @@
import SwiftUI
struct HomeNavigationCard: View {
let icon: String
let title: String
let subtitle: String
var body: some View {
HStack(spacing: 16) {
Image(systemName: icon)
.font(.system(size: 36))
.foregroundColor(.blue)
.frame(width: 60)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.title3)
.fontWeight(.semibold)
.foregroundColor(.primary)
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
}
.padding(20)
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
}
}

View File

@@ -0,0 +1,32 @@
import SwiftUI
struct ImageThumbnailView: View {
let image: UIImage
let onRemove: () -> Void
var body: some View {
ZStack(alignment: .topTrailing) {
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(.quaternary, lineWidth: 1)
}
Button(action: onRemove) {
Image(systemName: "xmark.circle.fill")
.font(.title3)
.foregroundStyle(.white)
.background {
Circle()
.fill(.black.opacity(0.6))
.padding(4)
}
}
.offset(x: 8, y: -8)
}
}
}

View File

@@ -0,0 +1,43 @@
import SwiftUI
import ComposeApp
struct OverviewCard: View {
let summary: OverallSummary
var body: some View {
VStack(spacing: 16) {
HStack {
Image(systemName: "chart.bar.fill")
.font(.title3)
Text("Overview")
.font(.title2)
.fontWeight(.bold)
Spacer()
}
HStack(spacing: 40) {
StatView(
icon: "house.fill",
value: "\(summary.totalResidences)",
label: "Properties"
)
StatView(
icon: "list.bullet",
value: "\(summary.totalTasks)",
label: "Total Tasks"
)
StatView(
icon: "clock.fill",
value: "\(summary.totalPending)",
label: "Pending"
)
}
}
.padding(20)
.background(Color.blue.opacity(0.1))
.cornerRadius(16)
.padding(.horizontal)
}
}

View File

@@ -0,0 +1,23 @@
import SwiftUI
struct StatView: View {
let icon: String
let value: String
let label: String
var body: some View {
VStack(spacing: 8) {
Image(systemName: icon)
.font(.title2)
.foregroundColor(.blue)
Text(value)
.font(.title)
.fontWeight(.bold)
Text(label)
.font(.caption)
.foregroundColor(.secondary)
}
}
}

View File

@@ -0,0 +1,53 @@
import SwiftUI
import ComposeApp
// MARK: - Share Code Card
struct ShareCodeCard: View {
let shareCode: ResidenceShareCode?
let residenceName: String
let isGeneratingCode: Bool
let onGenerateCode: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("Share Code")
.font(.subheadline)
.foregroundColor(.secondary)
if let shareCode = shareCode {
Text(shareCode.code)
.font(.title)
.fontWeight(.bold)
.foregroundColor(.blue)
} else {
Text("No active code")
.font(.body)
.foregroundColor(.secondary)
}
}
Spacer()
Button(action: onGenerateCode) {
HStack {
Image(systemName: "square.and.arrow.up")
Text(shareCode != nil ? "New Code" : "Generate")
}
}
.buttonStyle(.borderedProminent)
.disabled(isGeneratingCode)
}
if shareCode != nil {
Text("Share this code with others to give them access to \(residenceName)")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding()
.background(Color.blue.opacity(0.1))
.cornerRadius(12)
}
}

View File

@@ -0,0 +1,63 @@
import SwiftUI
import ComposeApp
// MARK: - User List Item
struct UserListItem: View {
let user: ResidenceUser
let isOwner: Bool
let isPrimaryOwner: Bool
let onRemove: () -> Void
var body: some View {
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(user.username)
.font(.body)
.fontWeight(.medium)
if isOwner {
Text("Owner")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.blue)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.blue.opacity(0.1))
.cornerRadius(4)
}
}
if !user.email.isEmpty {
Text(user.email)
.font(.caption)
.foregroundColor(.secondary)
}
let fullName = [user.firstName, user.lastName]
.compactMap { $0 }
.filter { !$0.isEmpty }
.joined(separator: " ")
if !fullName.isEmpty {
Text(fullName)
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
if isPrimaryOwner && !isOwner {
Button(action: onRemove) {
Image(systemName: "trash")
.foregroundColor(.red)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.padding(.horizontal)
}
}

View File

@@ -0,0 +1,159 @@
import SwiftUI
import ComposeApp
/// Task card that dynamically renders buttons based on the column's button types
struct DynamicTaskCard: View {
let task: TaskDetail
let buttonTypes: [String]
let onEdit: () -> Void
let onCancel: () -> Void
let onUncancel: () -> Void
let onMarkInProgress: () -> Void
let onComplete: () -> Void
let onArchive: () -> Void
let onUnarchive: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(task.title)
.font(.headline)
.foregroundColor(.primary)
if let status = task.status {
StatusBadge(status: status.name)
}
}
Spacer()
PriorityBadge(priority: task.priority.name)
}
if let description = task.description_, !description.isEmpty {
Text(description)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
}
HStack {
Label(task.frequency.displayName, systemImage: "repeat")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
if let due_date = task.dueDate {
Label(formatDate(due_date), systemImage: "calendar")
.font(.caption)
.foregroundColor(.secondary)
}
}
if task.completions.count > 0 {
Divider()
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("Completions (\(task.completions.count))")
.font(.caption)
.fontWeight(.semibold)
}
ForEach(task.completions, id: \.id) { completion in
CompletionCardView(completion: completion)
}
}
}
// Render buttons based on buttonTypes array
VStack(spacing: 8) {
ForEach(Array(buttonTypes.enumerated()), id: \.offset) { index, buttonType in
renderButton(for: buttonType)
}
}
}
.padding(16)
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2)
}
private func formatDate(_ dateString: String) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
if let date = formatter.date(from: dateString) {
formatter.dateStyle = .medium
return formatter.string(from: date)
}
return dateString
}
@ViewBuilder
private func renderButton(for buttonType: String) -> some View {
switch buttonType {
case "mark_in_progress":
MarkInProgressButton(
taskId: task.id,
onCompletion: onMarkInProgress,
onError: { error in
print("Error marking in progress: \(error)")
}
)
case "complete":
CompleteTaskButton(
taskId: task.id,
onCompletion: onComplete,
onError: { error in
print("Error completing task: \(error)")
}
)
case "edit":
EditTaskButton(
taskId: task.id,
onCompletion: onEdit,
onError: { error in
print("Error editing task: \(error)")
}
)
case "cancel":
CancelTaskButton(
taskId: task.id,
onCompletion: onCancel,
onError: { error in
print("Error cancelling task: \(error)")
}
)
case "uncancel":
UncancelTaskButton(
taskId: task.id,
onCompletion: onUncancel,
onError: { error in
print("Error restoring task: \(error)")
}
)
case "archive":
ArchiveTaskButton(
taskId: task.id,
onCompletion: onArchive,
onError: { error in
print("Error archiving task: \(error)")
}
)
case "unarchive":
UnarchiveTaskButton(
taskId: task.id,
onCompletion: onUnarchive,
onError: { error in
print("Error unarchiving task: \(error)")
}
)
default:
EmptyView()
}
}
}

View File

@@ -0,0 +1,81 @@
import SwiftUI
import ComposeApp
/// Dynamic task column view that adapts based on the column configuration
struct DynamicTaskColumnView: View {
let column: TaskColumn
let onEditTask: (TaskDetail) -> Void
let onCancelTask: (Int32) -> Void
let onUncancelTask: (Int32) -> Void
let onMarkInProgress: (Int32) -> Void
let onCompleteTask: (TaskDetail) -> Void
let onArchiveTask: (Int32) -> Void
let onUnarchiveTask: (Int32) -> Void
// Get icon from API response, with fallback
private var columnIcon: String {
column.icons["ios"] ?? "list.bullet"
}
private var columnColor: Color {
Color(hex: column.color) ?? .primary
}
var body: some View {
VStack(spacing: 0) {
ScrollView {
VStack(spacing: 16) {
// Header
HStack(spacing: 8) {
Image(systemName: columnIcon)
.font(.headline)
.foregroundColor(columnColor)
Text(column.displayName)
.font(.headline)
.foregroundColor(columnColor)
Spacer()
Text("\(column.count)")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.white)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(columnColor)
.cornerRadius(12)
}
if column.tasks.isEmpty {
VStack(spacing: 8) {
Image(systemName: columnIcon)
.font(.system(size: 40))
.foregroundColor(columnColor.opacity(0.3))
Text("No tasks")
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.top, 40)
} else {
ForEach(column.tasks, id: \.id) { task in
DynamicTaskCard(
task: task,
buttonTypes: column.buttonTypes,
onEdit: { onEditTask(task) },
onCancel: { onCancelTask(task.id) },
onUncancel: { onUncancelTask(task.id) },
onMarkInProgress: { onMarkInProgress(task.id) },
onComplete: { onCompleteTask(task) },
onArchive: { onArchiveTask(task.id) },
onUnarchive: { onUnarchiveTask(task.id) }
)
}
}
}
}
}
}
}

View File

@@ -198,242 +198,6 @@ struct AllTasksView: View {
}
}
/// Dynamic task column view that adapts based on the column configuration
struct DynamicTaskColumnView: View {
let column: TaskColumn
let onEditTask: (TaskDetail) -> Void
let onCancelTask: (Int32) -> Void
let onUncancelTask: (Int32) -> Void
let onMarkInProgress: (Int32) -> Void
let onCompleteTask: (TaskDetail) -> Void
let onArchiveTask: (Int32) -> Void
let onUnarchiveTask: (Int32) -> Void
// Get icon from API response, with fallback
private var columnIcon: String {
column.icons["ios"] ?? "list.bullet"
}
private var columnColor: Color {
Color(hex: column.color) ?? .primary
}
var body: some View {
VStack(spacing: 0) {
ScrollView {
VStack(spacing: 16) {
// Header
HStack(spacing: 8) {
Image(systemName: columnIcon)
.font(.headline)
.foregroundColor(columnColor)
Text(column.displayName)
.font(.headline)
.foregroundColor(columnColor)
Spacer()
Text("\(column.count)")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.white)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(columnColor)
.cornerRadius(12)
}
if column.tasks.isEmpty {
VStack(spacing: 8) {
Image(systemName: columnIcon)
.font(.system(size: 40))
.foregroundColor(columnColor.opacity(0.3))
Text("No tasks")
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.top, 40)
} else {
ForEach(column.tasks, id: \.id) { task in
DynamicTaskCard(
task: task,
buttonTypes: column.buttonTypes,
onEdit: { onEditTask(task) },
onCancel: { onCancelTask(task.id) },
onUncancel: { onUncancelTask(task.id) },
onMarkInProgress: { onMarkInProgress(task.id) },
onComplete: { onCompleteTask(task) },
onArchive: { onArchiveTask(task.id) },
onUnarchive: { onUnarchiveTask(task.id) }
)
}
}
}
}
}
}
}
/// Task card that dynamically renders buttons based on the column's button types
struct DynamicTaskCard: View {
let task: TaskDetail
let buttonTypes: [String]
let onEdit: () -> Void
let onCancel: () -> Void
let onUncancel: () -> Void
let onMarkInProgress: () -> Void
let onComplete: () -> Void
let onArchive: () -> Void
let onUnarchive: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(task.title)
.font(.headline)
.foregroundColor(.primary)
if let status = task.status {
StatusBadge(status: status.name)
}
}
Spacer()
PriorityBadge(priority: task.priority.name)
}
if let description = task.description_, !description.isEmpty {
Text(description)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
}
HStack {
Label(task.frequency.displayName, systemImage: "repeat")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
if let due_date = task.dueDate {
Label(formatDate(due_date), systemImage: "calendar")
.font(.caption)
.foregroundColor(.secondary)
}
}
if task.completions.count > 0 {
Divider()
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("Completions (\(task.completions.count))")
.font(.caption)
.fontWeight(.semibold)
}
ForEach(task.completions, id: \.id) { completion in
CompletionCardView(completion: completion)
}
}
}
// Render buttons based on buttonTypes array
VStack(spacing: 8) {
ForEach(Array(buttonTypes.enumerated()), id: \.offset) { index, buttonType in
renderButton(for: buttonType)
}
}
}
.padding(16)
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2)
}
private func formatDate(_ dateString: String) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
if let date = formatter.date(from: dateString) {
formatter.dateStyle = .medium
return formatter.string(from: date)
}
return dateString
}
@ViewBuilder
private func renderButton(for buttonType: String) -> some View {
switch buttonType {
case "mark_in_progress":
MarkInProgressButton(
taskId: task.id,
onCompletion: onMarkInProgress,
onError: { error in
print("Error marking in progress: \(error)")
}
)
case "complete":
CompleteTaskButton(
taskId: task.id,
onCompletion: onComplete,
onError: { error in
print("Error completing task: \(error)")
}
)
case "edit":
EditTaskButton(
taskId: task.id,
onCompletion: onEdit,
onError: { error in
print("Error editing task: \(error)")
}
)
case "cancel":
CancelTaskButton(
taskId: task.id,
onCompletion: onCancel,
onError: { error in
print("Error cancelling task: \(error)")
}
)
case "uncancel":
UncancelTaskButton(
taskId: task.id,
onCompletion: onUncancel,
onError: { error in
print("Error restoring task: \(error)")
}
)
case "archive":
ArchiveTaskButton(
taskId: task.id,
onCompletion: onArchive,
onError: { error in
print("Error archiving task: \(error)")
}
)
case "unarchive":
UnarchiveTaskButton(
taskId: task.id,
onCompletion: onUnarchive,
onError: { error in
print("Error unarchiving task: \(error)")
}
)
default:
EmptyView()
}
}
}
// Extension to apply corner radius to specific corners
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {

View File

@@ -310,72 +310,3 @@ extension KotlinByteArray {
}
}
// Image Thumbnail View Component
struct ImageThumbnailView: View {
let image: UIImage
let onRemove: () -> Void
var body: some View {
ZStack(alignment: .topTrailing) {
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(.quaternary, lineWidth: 1)
}
Button(action: onRemove) {
Image(systemName: "xmark.circle.fill")
.font(.title3)
.foregroundStyle(.white)
.background {
Circle()
.fill(.black.opacity(0.6))
.padding(4)
}
}
.offset(x: 8, y: -8)
}
}
}
// Camera Picker View Component
struct CameraPickerView: UIViewControllerRepresentable {
@Environment(\.dismiss) var dismiss
let onImageCaptured: (UIImage) -> Void
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: CameraPickerView
init(_ parent: CameraPickerView) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[.originalImage] as? UIImage {
parent.onImageCaptured(image)
}
parent.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parent.dismiss()
}
}
}