- Update DEV API URLs from treytartt.com to api.myhoneydue.com - Add rounded corners to app icon in login, register, and onboarding screens - Add 9 missing English translations in Localizable.xcstrings - Fix property feature pills to use equal height for balanced layout - Remove duplicate honeyDue user scheme Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
334 lines
12 KiB
Swift
334 lines
12 KiB
Swift
import SwiftUI
|
|
import ComposeApp
|
|
|
|
struct ResidenceCard: View {
|
|
let residence: ResidenceResponse
|
|
let taskMetrics: WidgetDataManager.TaskMetrics
|
|
|
|
/// Check if this residence has any overdue tasks
|
|
private var hasOverdueTasks: Bool {
|
|
taskMetrics.overdueCount > 0
|
|
}
|
|
|
|
/// Open the address in Apple Maps
|
|
private func openInMaps() {
|
|
var addressComponents: [String] = []
|
|
if !residence.streetAddress.isEmpty {
|
|
addressComponents.append(residence.streetAddress)
|
|
}
|
|
if !residence.city.isEmpty {
|
|
addressComponents.append(residence.city)
|
|
}
|
|
if !residence.stateProvince.isEmpty {
|
|
addressComponents.append(residence.stateProvince)
|
|
}
|
|
if !residence.postalCode.isEmpty {
|
|
addressComponents.append(residence.postalCode)
|
|
}
|
|
|
|
let address = addressComponents.joined(separator: ", ")
|
|
guard let encodedAddress = address.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
|
|
let url = URL(string: "maps://?address=\(encodedAddress)") else {
|
|
return
|
|
}
|
|
|
|
UIApplication.shared.open(url)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
// Top Section: Icon + Property Info + Primary Badge
|
|
HStack(alignment: .top, spacing: 16) {
|
|
// Property Icon with organic styling
|
|
PropertyIconView(hasOverdue: hasOverdueTasks)
|
|
|
|
// Property Details
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
// Property Name
|
|
Text(residence.name)
|
|
.font(.system(size: 20, weight: .bold, design: .rounded))
|
|
.foregroundColor(Color.appTextPrimary)
|
|
.lineLimit(1)
|
|
|
|
// Property Type
|
|
if let propertyTypeName = residence.propertyTypeName {
|
|
Text(propertyTypeName.uppercased())
|
|
.font(.system(size: 11, weight: .semibold, design: .rounded))
|
|
.foregroundColor(Color.appTextSecondary)
|
|
.tracking(1.2)
|
|
}
|
|
|
|
// Address - tappable to open maps
|
|
if !residence.streetAddress.isEmpty {
|
|
Button(action: {
|
|
openInMaps()
|
|
}) {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: "mappin")
|
|
.font(.system(size: 10, weight: .bold))
|
|
.foregroundColor(Color.appPrimary.opacity(0.7))
|
|
|
|
Text(residence.streetAddress)
|
|
.font(.system(size: 13, weight: .medium))
|
|
.foregroundColor(Color.appPrimary)
|
|
.lineLimit(1)
|
|
|
|
Image(systemName: "arrow.up.right")
|
|
.font(.system(size: 9, weight: .semibold))
|
|
.foregroundColor(Color.appPrimary.opacity(0.6))
|
|
}
|
|
}
|
|
.padding(.top, 2)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Primary Badge
|
|
if residence.isPrimary {
|
|
PrimaryBadgeView()
|
|
}
|
|
}
|
|
.padding(.horizontal, OrganicSpacing.cozy)
|
|
.padding(.top, OrganicSpacing.cozy)
|
|
.padding(.bottom, 16)
|
|
|
|
// Divider
|
|
OrganicDivider()
|
|
.padding(.horizontal, 16)
|
|
|
|
// Task Stats Section
|
|
if taskMetrics.totalCount > 0 {
|
|
HStack(spacing: 0) {
|
|
// Total Tasks
|
|
TaskStatItem(
|
|
value: taskMetrics.totalCount,
|
|
label: "Tasks",
|
|
color: Color.appPrimary
|
|
)
|
|
|
|
// Overdue
|
|
TaskStatItem(
|
|
value: taskMetrics.overdueCount,
|
|
label: "Overdue",
|
|
color: taskMetrics.overdueCount > 0 ? Color.appError : Color.appTextSecondary
|
|
)
|
|
|
|
// Due Next 7 Days
|
|
TaskStatItem(
|
|
value: taskMetrics.upcoming7Days,
|
|
label: "7 Days",
|
|
color: Color.appAccent
|
|
)
|
|
|
|
// Next 30 Days
|
|
TaskStatItem(
|
|
value: taskMetrics.upcoming30Days,
|
|
label: "30 Days",
|
|
color: Color.appPrimary.opacity(0.7)
|
|
)
|
|
}
|
|
.padding(.horizontal, OrganicSpacing.cozy)
|
|
.padding(.vertical, 14)
|
|
} else {
|
|
// Empty state for tasks
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "checkmark.circle")
|
|
.font(.system(size: 14, weight: .medium))
|
|
.foregroundColor(Color.appPrimary.opacity(0.5))
|
|
|
|
Text("No tasks yet")
|
|
.font(.system(size: 13, weight: .medium, design: .rounded))
|
|
.foregroundColor(Color.appTextSecondary)
|
|
}
|
|
.padding(.horizontal, OrganicSpacing.cozy)
|
|
.padding(.vertical, 16)
|
|
}
|
|
}
|
|
.background(CardBackgroundView(hasOverdue: hasOverdueTasks))
|
|
.clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
|
|
.naturalShadow(.medium)
|
|
}
|
|
}
|
|
|
|
// MARK: - Property Icon View
|
|
|
|
private struct PropertyIconView: View {
|
|
let hasOverdue: Bool
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Theme-colored background
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [
|
|
Color.appPrimary,
|
|
Color.appPrimary.opacity(0.85)
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
)
|
|
.frame(width: 56, height: 56)
|
|
|
|
// House icon
|
|
Image("outline")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 48, height: 48)
|
|
.foregroundColor(Color.appTextOnPrimary)
|
|
}
|
|
.naturalShadow(.subtle)
|
|
}
|
|
}
|
|
|
|
// MARK: - Primary Badge
|
|
|
|
private struct PrimaryBadgeView: View {
|
|
var body: some View {
|
|
ZStack {
|
|
// Soft background
|
|
Circle()
|
|
.fill(Color.appAccent.opacity(0.15))
|
|
.frame(width: 36, height: 36)
|
|
|
|
// Star icon
|
|
Image(systemName: "star.fill")
|
|
.font(.system(size: 14, weight: .bold))
|
|
.foregroundColor(Color.appAccent)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Task Stat Item
|
|
|
|
private struct TaskStatItem: View {
|
|
let value: Int
|
|
let label: String
|
|
let color: Color
|
|
|
|
var body: some View {
|
|
HStack(spacing: 4) {
|
|
Text("\(value)")
|
|
.font(.system(size: 15, weight: .bold, design: .rounded))
|
|
.foregroundColor(color)
|
|
|
|
Text(label)
|
|
.font(.system(size: 11, weight: .medium))
|
|
.foregroundColor(Color.appTextSecondary)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
|
|
// MARK: - Card Background
|
|
|
|
private struct CardBackgroundView: View {
|
|
let hasOverdue: Bool
|
|
@Environment(\.colorScheme) var colorScheme
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Base fill
|
|
Color.appBackgroundSecondary
|
|
|
|
// Subtle organic blob accent in corner
|
|
GeometryReader { geo in
|
|
OrganicBlobShape(variation: 1)
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [
|
|
Color.appPrimary.opacity(colorScheme == .dark ? 0.06 : 0.04),
|
|
Color.appPrimary.opacity(0.01)
|
|
],
|
|
startPoint: .topTrailing,
|
|
endPoint: .bottomLeading
|
|
)
|
|
)
|
|
.frame(width: geo.size.width * 0.5, height: geo.size.height * 0.6)
|
|
.offset(x: geo.size.width * 0.55, y: -geo.size.height * 0.05)
|
|
.blur(radius: 15)
|
|
}
|
|
|
|
// Subtle grain texture
|
|
GrainTexture(opacity: 0.012)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview("Residence Card") {
|
|
ScrollView {
|
|
VStack(spacing: 20) {
|
|
ResidenceCard(
|
|
residence: ResidenceResponse(
|
|
id: 1,
|
|
ownerId: 1,
|
|
owner: ResidenceUserResponse(id: 1, username: "testuser", email: "test@test.com", firstName: "John", lastName: "Doe"),
|
|
users: [],
|
|
name: "Sunset Villa",
|
|
propertyTypeId: 1,
|
|
propertyType: ResidenceType(id: 1, name: "House"),
|
|
streetAddress: "742 Evergreen Terrace",
|
|
apartmentUnit: "",
|
|
city: "San Francisco",
|
|
stateProvince: "CA",
|
|
postalCode: "94102",
|
|
country: "USA",
|
|
bedrooms: 4,
|
|
bathrooms: 2.5,
|
|
squareFootage: 2400,
|
|
lotSize: 0.35,
|
|
yearBuilt: 2018,
|
|
description: "Beautiful modern home",
|
|
purchaseDate: nil,
|
|
purchasePrice: nil,
|
|
isPrimary: true,
|
|
isActive: true,
|
|
overdueCount: 2,
|
|
createdAt: "2024-01-01T00:00:00Z",
|
|
updatedAt: "2024-01-01T00:00:00Z"
|
|
),
|
|
taskMetrics: WidgetDataManager.TaskMetrics(totalCount: 8, overdueCount: 2, upcoming7Days: 3, upcoming30Days: 2)
|
|
)
|
|
|
|
ResidenceCard(
|
|
residence: ResidenceResponse(
|
|
id: 2,
|
|
ownerId: 1,
|
|
owner: ResidenceUserResponse(id: 1, username: "testuser", email: "test@test.com", firstName: "John", lastName: "Doe"),
|
|
users: [],
|
|
name: "Downtown Loft",
|
|
propertyTypeId: 2,
|
|
propertyType: ResidenceType(id: 2, name: "Apartment"),
|
|
streetAddress: "100 Market Street, Unit 502",
|
|
apartmentUnit: "502",
|
|
city: "San Francisco",
|
|
stateProvince: "CA",
|
|
postalCode: "94105",
|
|
country: "USA",
|
|
bedrooms: 2,
|
|
bathrooms: 1.0,
|
|
squareFootage: 1100,
|
|
lotSize: nil,
|
|
yearBuilt: 2020,
|
|
description: "",
|
|
purchaseDate: nil,
|
|
purchasePrice: nil,
|
|
isPrimary: false,
|
|
isActive: true,
|
|
overdueCount: 0,
|
|
createdAt: "2024-01-01T00:00:00Z",
|
|
updatedAt: "2024-01-01T00:00:00Z"
|
|
),
|
|
taskMetrics: WidgetDataManager.TaskMetrics(totalCount: 0, overdueCount: 0, upcoming7Days: 0, upcoming30Days: 0)
|
|
)
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 24)
|
|
}
|
|
.background(WarmGradientBackground())
|
|
}
|