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 {
|
object ApiConfig {
|
||||||
// ⚠️ CHANGE THIS TO TOGGLE ENVIRONMENT ⚠️
|
// ⚠️ CHANGE THIS TO TOGGLE ENVIRONMENT ⚠️
|
||||||
val CURRENT_ENV = Environment.LOCAL
|
val CURRENT_ENV = Environment.DEV
|
||||||
|
|
||||||
enum class Environment {
|
enum class Environment {
|
||||||
LOCAL,
|
LOCAL,
|
||||||
|
|||||||
@@ -8,6 +8,53 @@
|
|||||||
import WidgetKit
|
import WidgetKit
|
||||||
import SwiftUI
|
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
|
/// CacheManager reads tasks from the App Group shared container
|
||||||
/// Data is written by the main app via WidgetDataManager
|
/// Data is written by the main app via WidgetDataManager
|
||||||
class CacheManager {
|
class CacheManager {
|
||||||
@@ -192,7 +239,7 @@ struct SmallWidgetView: View {
|
|||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: "calendar")
|
Image(systemName: "calendar")
|
||||||
.font(.system(size: 9))
|
.font(.system(size: 9))
|
||||||
Text(formatDate(dueDate))
|
Text(formatWidgetDate(dueDate))
|
||||||
.font(.system(size: 10, weight: .medium))
|
.font(.system(size: 10, weight: .medium))
|
||||||
}
|
}
|
||||||
.foregroundStyle(.orange)
|
.foregroundStyle(.orange)
|
||||||
@@ -225,27 +272,6 @@ struct SmallWidgetView: View {
|
|||||||
}
|
}
|
||||||
.padding(16)
|
.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
|
// MARK: - Medium Widget View
|
||||||
@@ -338,15 +364,15 @@ struct TaskRowView: View {
|
|||||||
HStack(spacing: 3) {
|
HStack(spacing: 3) {
|
||||||
Image(systemName: "calendar")
|
Image(systemName: "calendar")
|
||||||
.font(.system(size: 8))
|
.font(.system(size: 8))
|
||||||
Text(formatDate(dueDate))
|
Text(formatWidgetDate(dueDate))
|
||||||
.font(.system(size: 9, weight: .medium))
|
.font(.system(size: 9, weight: .medium))
|
||||||
}
|
}
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if let priority = task.priority {
|
if let priority = task.priority {
|
||||||
Text(priority.prefix(1).uppercased())
|
Text(priority.prefix(1).uppercased())
|
||||||
.font(.system(size: 9, weight: .bold))
|
.font(.system(size: 9, weight: .bold))
|
||||||
@@ -360,7 +386,7 @@ struct TaskRowView: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var priorityColor: Color {
|
private var priorityColor: Color {
|
||||||
switch task.priority?.lowercased() {
|
switch task.priority?.lowercased() {
|
||||||
case "urgent": return .red
|
case "urgent": return .red
|
||||||
@@ -369,27 +395,6 @@ struct TaskRowView: View {
|
|||||||
default: return .gray
|
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
|
// MARK: - Large Widget View
|
||||||
@@ -483,7 +488,7 @@ struct LargeTaskRowView: View {
|
|||||||
HStack(spacing: 2) {
|
HStack(spacing: 2) {
|
||||||
Image(systemName: "calendar")
|
Image(systemName: "calendar")
|
||||||
.font(.system(size: 7))
|
.font(.system(size: 7))
|
||||||
Text(formatDate(dueDate))
|
Text(formatWidgetDate(dueDate))
|
||||||
.font(.system(size: 9, weight: isOverdue ? .semibold : .regular))
|
.font(.system(size: 9, weight: isOverdue ? .semibold : .regular))
|
||||||
}
|
}
|
||||||
.foregroundStyle(isOverdue ? .red : .secondary)
|
.foregroundStyle(isOverdue ? .red : .secondary)
|
||||||
@@ -518,28 +523,6 @@ struct LargeTaskRowView: View {
|
|||||||
default: return .gray
|
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 {
|
struct Casera: Widget {
|
||||||
|
|||||||
@@ -150,15 +150,32 @@ final class WidgetDataManager {
|
|||||||
private func isTaskOverdue(dueDate: String?, status: String?) -> Bool {
|
private func isTaskOverdue(dueDate: String?, status: String?) -> Bool {
|
||||||
guard let dueDateStr = dueDate else { return false }
|
guard let dueDateStr = dueDate else { return false }
|
||||||
|
|
||||||
let formatter = DateFormatter()
|
var date: Date?
|
||||||
formatter.dateFormat = "yyyy-MM-dd"
|
|
||||||
|
|
||||||
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
|
// Task is overdue if due date is in the past and status is not completed
|
||||||
let statusLower = status?.lowercased() ?? ""
|
let statusLower = status?.lowercased() ?? ""
|
||||||
let isCompleted = statusLower == "completed" || statusLower == "done"
|
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