Fix iOS widget date formatting for RFC3339 dates
- Add centralized formatWidgetDate() helper that handles both yyyy-MM-dd and ISO8601 formats - Update widget date display to show "Today", "in X days", or "X days ago" - Fix isTaskOverdue() to parse ISO8601 dates from Go API - Remove duplicate date formatting functions from widget views - Switch API environment back to DEV 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ package com.example.casera.network
|
||||
*/
|
||||
object ApiConfig {
|
||||
// ⚠️ CHANGE THIS TO TOGGLE ENVIRONMENT ⚠️
|
||||
val CURRENT_ENV = Environment.LOCAL
|
||||
val CURRENT_ENV = Environment.DEV
|
||||
|
||||
enum class Environment {
|
||||
LOCAL,
|
||||
|
||||
@@ -8,6 +8,53 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Date Formatting Helper
|
||||
/// Parses date strings in either yyyy-MM-dd or ISO8601 (RFC3339) format
|
||||
/// and returns a user-friendly string like "Today" or "in X days"
|
||||
private func formatWidgetDate(_ dateString: String) -> String {
|
||||
let calendar = Calendar.current
|
||||
var date: Date?
|
||||
|
||||
// Try parsing as yyyy-MM-dd first
|
||||
let dateOnlyFormatter = DateFormatter()
|
||||
dateOnlyFormatter.dateFormat = "yyyy-MM-dd"
|
||||
date = dateOnlyFormatter.date(from: dateString)
|
||||
|
||||
// Try parsing as ISO8601 (RFC3339) if that fails
|
||||
if date == nil {
|
||||
let isoFormatter = ISO8601DateFormatter()
|
||||
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
date = isoFormatter.date(from: dateString)
|
||||
|
||||
// Try without fractional seconds
|
||||
if date == nil {
|
||||
isoFormatter.formatOptions = [.withInternetDateTime]
|
||||
date = isoFormatter.date(from: dateString)
|
||||
}
|
||||
}
|
||||
|
||||
guard let parsedDate = date else {
|
||||
return dateString
|
||||
}
|
||||
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let dueDay = calendar.startOfDay(for: parsedDate)
|
||||
|
||||
if calendar.isDateInToday(parsedDate) {
|
||||
return "Today"
|
||||
}
|
||||
|
||||
let components = calendar.dateComponents([.day], from: today, to: dueDay)
|
||||
let days = components.day ?? 0
|
||||
|
||||
if days > 0 {
|
||||
return days == 1 ? "in 1 day" : "in \(days) days"
|
||||
} else {
|
||||
let overdueDays = abs(days)
|
||||
return overdueDays == 1 ? "1 day ago" : "\(overdueDays) days ago"
|
||||
}
|
||||
}
|
||||
|
||||
/// CacheManager reads tasks from the App Group shared container
|
||||
/// Data is written by the main app via WidgetDataManager
|
||||
class CacheManager {
|
||||
@@ -192,7 +239,7 @@ struct SmallWidgetView: View {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "calendar")
|
||||
.font(.system(size: 9))
|
||||
Text(formatDate(dueDate))
|
||||
Text(formatWidgetDate(dueDate))
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
}
|
||||
.foregroundStyle(.orange)
|
||||
@@ -225,27 +272,6 @@ struct SmallWidgetView: View {
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
|
||||
private func formatDate(_ dateString: String) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
|
||||
if let date = formatter.date(from: dateString) {
|
||||
let now = Date()
|
||||
let calendar = Calendar.current
|
||||
|
||||
if calendar.isDateInToday(date) {
|
||||
return "Today"
|
||||
} else if calendar.isDateInTomorrow(date) {
|
||||
return "Tomorrow"
|
||||
} else {
|
||||
formatter.dateFormat = "MMM d"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
}
|
||||
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Medium Widget View
|
||||
@@ -338,15 +364,15 @@ struct TaskRowView: View {
|
||||
HStack(spacing: 3) {
|
||||
Image(systemName: "calendar")
|
||||
.font(.system(size: 8))
|
||||
Text(formatDate(dueDate))
|
||||
Text(formatWidgetDate(dueDate))
|
||||
.font(.system(size: 9, weight: .medium))
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
if let priority = task.priority {
|
||||
Text(priority.prefix(1).uppercased())
|
||||
.font(.system(size: 9, weight: .bold))
|
||||
@@ -360,7 +386,7 @@ struct TaskRowView: View {
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
|
||||
private var priorityColor: Color {
|
||||
switch task.priority?.lowercased() {
|
||||
case "urgent": return .red
|
||||
@@ -369,27 +395,6 @@ struct TaskRowView: View {
|
||||
default: return .gray
|
||||
}
|
||||
}
|
||||
|
||||
private func formatDate(_ dateString: String) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
|
||||
if let date = formatter.date(from: dateString) {
|
||||
let now = Date()
|
||||
let calendar = Calendar.current
|
||||
|
||||
if calendar.isDateInToday(date) {
|
||||
return "Today"
|
||||
} else if calendar.isDateInTomorrow(date) {
|
||||
return "Tomorrow"
|
||||
} else {
|
||||
formatter.dateFormat = "MMM d"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
}
|
||||
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Large Widget View
|
||||
@@ -483,7 +488,7 @@ struct LargeTaskRowView: View {
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "calendar")
|
||||
.font(.system(size: 7))
|
||||
Text(formatDate(dueDate))
|
||||
Text(formatWidgetDate(dueDate))
|
||||
.font(.system(size: 9, weight: isOverdue ? .semibold : .regular))
|
||||
}
|
||||
.foregroundStyle(isOverdue ? .red : .secondary)
|
||||
@@ -518,28 +523,6 @@ struct LargeTaskRowView: View {
|
||||
default: return .gray
|
||||
}
|
||||
}
|
||||
|
||||
private func formatDate(_ dateString: String) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
|
||||
if let date = formatter.date(from: dateString) {
|
||||
let calendar = Calendar.current
|
||||
|
||||
if calendar.isDateInToday(date) {
|
||||
return "Today"
|
||||
} else if calendar.isDateInTomorrow(date) {
|
||||
return "Tomorrow"
|
||||
} else if calendar.isDateInYesterday(date) {
|
||||
return "Yesterday"
|
||||
} else {
|
||||
formatter.dateFormat = "MMM d"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
}
|
||||
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
struct Casera: Widget {
|
||||
|
||||
@@ -150,15 +150,32 @@ final class WidgetDataManager {
|
||||
private func isTaskOverdue(dueDate: String?, status: String?) -> Bool {
|
||||
guard let dueDateStr = dueDate else { return false }
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
var date: Date?
|
||||
|
||||
guard let date = formatter.date(from: dueDateStr) else { return false }
|
||||
// Try parsing as yyyy-MM-dd first
|
||||
let dateOnlyFormatter = DateFormatter()
|
||||
dateOnlyFormatter.dateFormat = "yyyy-MM-dd"
|
||||
date = dateOnlyFormatter.date(from: dueDateStr)
|
||||
|
||||
// Try parsing as ISO8601 (RFC3339) if that fails
|
||||
if date == nil {
|
||||
let isoFormatter = ISO8601DateFormatter()
|
||||
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
date = isoFormatter.date(from: dueDateStr)
|
||||
|
||||
// Try without fractional seconds
|
||||
if date == nil {
|
||||
isoFormatter.formatOptions = [.withInternetDateTime]
|
||||
date = isoFormatter.date(from: dueDateStr)
|
||||
}
|
||||
}
|
||||
|
||||
guard let parsedDate = date else { return false }
|
||||
|
||||
// Task is overdue if due date is in the past and status is not completed
|
||||
let statusLower = status?.lowercased() ?? ""
|
||||
let isCompleted = statusLower == "completed" || statusLower == "done"
|
||||
|
||||
return !isCompleted && date < Calendar.current.startOfDay(for: Date())
|
||||
return !isCompleted && parsedDate < Calendar.current.startOfDay(for: Date())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user