diff options
| -rw-r--r-- | internal/handlers/handlers.go | 4 | ||||
| -rw-r--r-- | internal/handlers/tabs.go | 31 | ||||
| -rw-r--r-- | internal/models/atom.go | 21 | ||||
| -rw-r--r-- | web/templates/index.html | 42 | ||||
| -rw-r--r-- | web/templates/partials/tasks-tab.html | 81 |
5 files changed, 97 insertions, 82 deletions
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 8d809ae..e4d6457 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -762,10 +762,10 @@ func (h *Handler) HandleUnifiedAdd(w http.ResponseWriter, r *http.Request) { return } - // Parse due date if provided + // Parse due date if provided (use local timezone) var dueDate *time.Time if dueDateStr != "" { - parsed, err := time.Parse("2006-01-02", dueDateStr) + parsed, err := time.ParseInLocation("2006-01-02", dueDateStr, time.Local) if err == nil { dueDate = &parsed } diff --git a/internal/handlers/tabs.go b/internal/handlers/tabs.go index 7e0b352..bd15710 100644 --- a/internal/handlers/tabs.go +++ b/internal/handlers/tabs.go @@ -88,7 +88,12 @@ func (h *TabsHandler) HandleTasks(w http.ResponseWriter, r *http.Request) { } } - // Sort atoms: by DueDate (earliest first), then by Priority (descending) + // Compute UI fields (IsOverdue, HasSetTime) + for i := range atoms { + atoms[i].ComputeUIFields() + } + + // Sort atoms: by DueDate (earliest first), then by HasSetTime, then by Priority sort.SliceStable(atoms, func(i, j int) bool { // Handle nil due dates (push to end) if atoms[i].DueDate == nil && atoms[j].DueDate != nil { @@ -98,14 +103,30 @@ func (h *TabsHandler) HandleTasks(w http.ResponseWriter, r *http.Request) { return true } - // Both have due dates, sort by date + // Both have due dates if atoms[i].DueDate != nil && atoms[j].DueDate != nil { - if !atoms[i].DueDate.Equal(*atoms[j].DueDate) { - return atoms[i].DueDate.Before(*atoms[j].DueDate) + // Compare by date only (ignore time) + dateI := atoms[i].DueDate.Truncate(24 * time.Hour) + dateJ := atoms[j].DueDate.Truncate(24 * time.Hour) + + if !dateI.Equal(dateJ) { + return dateI.Before(dateJ) + } + + // Same day: tasks with set times come before midnight tasks + if atoms[i].HasSetTime != atoms[j].HasSetTime { + return atoms[i].HasSetTime + } + + // Both have set times or both are midnight, sort by actual time + if atoms[i].HasSetTime && atoms[j].HasSetTime { + if !atoms[i].DueDate.Equal(*atoms[j].DueDate) { + return atoms[i].DueDate.Before(*atoms[j].DueDate) + } } } - // Same due date (or both nil), sort by priority (descending) + // Same due date/time (or both nil), sort by priority (descending) return atoms[i].Priority > atoms[j].Priority }) diff --git a/internal/models/atom.go b/internal/models/atom.go index fe40962..b3a384a 100644 --- a/internal/models/atom.go +++ b/internal/models/atom.go @@ -36,11 +36,30 @@ type Atom struct { // UI Helpers (to be populated by mappers) SourceIcon string // e.g., "trello-icon.svg" or emoji ColorClass string // e.g., "border-blue-500" + IsOverdue bool // True if due date is before today + HasSetTime bool // True if due time is not midnight (has specific time) // Original Data (for write operations) Raw interface{} } +// ComputeUIFields calculates IsOverdue and HasSetTime based on DueDate +func (a *Atom) ComputeUIFields() { + if a.DueDate == nil { + return + } + + now := time.Now() + today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + + // Check if overdue (due date is before today) + dueDay := time.Date(a.DueDate.Year(), a.DueDate.Month(), a.DueDate.Day(), 0, 0, 0, 0, a.DueDate.Location()) + a.IsOverdue = dueDay.Before(today) + + // Check if has set time (not midnight) + a.HasSetTime = a.DueDate.Hour() != 0 || a.DueDate.Minute() != 0 +} + // TaskToAtom converts a Todoist Task to an Atom func TaskToAtom(t Task) Atom { // Todoist priority: 1 (normal) to 4 (urgent) @@ -63,7 +82,7 @@ func TaskToAtom(t Task) Atom { DueDate: t.DueDate, CreatedAt: t.CreatedAt, Priority: priority, - SourceIcon: "✓", // Checkmark emoji for tasks + SourceIcon: "🔴", // Red circle for Todoist ColorClass: "border-red-500", Raw: t, } diff --git a/web/templates/index.html b/web/templates/index.html index 2cd4c59..18aa56b 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -8,43 +8,25 @@ <link rel="stylesheet" href="/static/css/output.css"> </head> <body class="min-h-screen" hx-headers='{"X-CSRF-Token": "{{.CSRFToken}}"}'> - <div class="content-max-width py-4 sm:py-8"> - <!-- Header - Hidden on mobile --> - <header class="hidden sm:flex mb-8 flex-row justify-between items-center gap-4"> - <h1 class="text-4xl font-bold text-gray-900">Personal Dashboard</h1> - <div class="flex items-center gap-4"> - <span class="text-sm text-gray-600"> - Last updated: <span id="last-updated">{{.LastUpdated.Format "3:04 PM"}}</span> - </span> - <button onclick="refreshData()" - class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg transition-colors font-medium no-print"> - <span id="refresh-text">Refresh</span> - </button> - <form method="POST" action="/logout" class="no-print"> - <input type="hidden" name="csrf_token" value="{{.CSRFToken}}"> - <button type="submit" - class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-lg transition-colors font-medium"> - Logout - </button> - </form> - </div> - </header> - - <!-- Mobile Header - Compact --> - <header class="flex sm:hidden mb-4 justify-between items-center"> + <div class="content-max-width py-3 sm:py-6"> + <!-- Minimal Header --> + <header class="flex mb-4 sm:mb-6 justify-between items-center no-print"> <button onclick="refreshData()" - class="text-primary-600 p-2 no-print"> - <span id="refresh-text-mobile">↻</span> + class="text-gray-400 hover:text-primary-600 transition-colors p-1" + title="Refresh"> + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path> + </svg> </button> - <span class="text-xs text-gray-500" id="last-updated-mobile">{{.LastUpdated.Format "3:04 PM"}}</span> - <form method="POST" action="/logout" class="no-print"> + <span class="text-xs text-gray-400" id="last-updated">{{.LastUpdated.Format "3:04 PM"}}</span> + <form method="POST" action="/logout"> <input type="hidden" name="csrf_token" value="{{.CSRFToken}}"> - <button type="submit" class="text-gray-500 text-sm p-2">Logout</button> + <button type="submit" class="text-gray-400 hover:text-gray-600 transition-colors text-xs">Logout</button> </form> </header> <!-- Tab Navigation --> - <div class="mb-8 no-print"> + <div class="mb-4 sm:mb-6 no-print"> <nav class="flex space-x-1 bg-white/30 backdrop-blur-md rounded-xl p-1"> <button class="tab-button {{if eq .ActiveTab "tasks"}}tab-button-active{{end}}" diff --git a/web/templates/partials/tasks-tab.html b/web/templates/partials/tasks-tab.html index 3f0d5bf..2a89a40 100644 --- a/web/templates/partials/tasks-tab.html +++ b/web/templates/partials/tasks-tab.html @@ -69,54 +69,47 @@ </form> </section> - <!-- Unified Tasks Section --> - <section> - <h2 class="text-base sm:text-lg font-semibold text-gray-900 mb-3 sm:mb-4"> - Upcoming Tasks - </h2> - - {{if .Atoms}} - <div class="space-y-2 sm:space-y-3"> - {{range .Atoms}} - <div class="task-item bg-white rounded-lg p-3 sm:p-4 shadow-sm hover:shadow-md transition-shadow border-l-4 {{.ColorClass}}" - hx-post="/complete-atom" - hx-trigger="click" - hx-vals='{"id": "{{.ID}}", "source": "{{.Source}}"}' - hx-target="this" - hx-swap="outerHTML" - hx-confirm="Mark as complete?"> - <div class="flex items-start gap-2 sm:gap-3"> - <span class="text-lg sm:text-xl flex-shrink-0">{{.SourceIcon}}</span> - <div class="flex-1 min-w-0"> - <div class="flex items-start justify-between gap-2"> - <h3 class="text-sm sm:text-base font-medium text-gray-900 break-words">{{.Title}}</h3> - {{if .URL}} - <a href="{{.URL}}" target="_blank" class="text-primary-600 hover:text-primary-800 flex-shrink-0" onclick="event.stopPropagation()"> - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path> - </svg> - </a> - {{end}} - </div> - - <div class="flex flex-wrap items-center gap-2 sm:gap-3 mt-1 text-xs sm:text-sm text-gray-500"> - {{if .DueDate}} - <span>{{.DueDate.Format "Jan 2"}}</span> - {{end}} - {{if gt .Priority 2}} - <span class="text-red-600 font-medium">P{{.Priority}}</span> - {{end}} - </div> + <!-- Tasks List --> + {{if .Atoms}} + <div class="space-y-2"> + {{range .Atoms}} + <div class="task-item bg-white rounded-lg p-3 sm:p-4 shadow-sm hover:shadow-md transition-shadow border-l-4 {{.ColorClass}} {{if .IsOverdue}}opacity-50{{end}}" + hx-post="/complete-atom" + hx-trigger="click" + hx-vals='{"id": "{{.ID}}", "source": "{{.Source}}"}' + hx-target="this" + hx-swap="outerHTML" + hx-confirm="Mark as complete?"> + <div class="flex items-start gap-2 sm:gap-3"> + <span class="text-lg flex-shrink-0">{{.SourceIcon}}</span> + <div class="flex-1 min-w-0"> + <div class="flex items-start justify-between gap-2"> + <h3 class="text-sm font-medium {{if .IsOverdue}}text-gray-500{{else}}text-gray-900{{end}} break-words">{{.Title}}</h3> + {{if .URL}} + <a href="{{.URL}}" target="_blank" class="text-primary-600 hover:text-primary-800 flex-shrink-0" onclick="event.stopPropagation()"> + <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path> + </svg> + </a> + {{end}} + </div> + <div class="flex flex-wrap items-center gap-2 mt-1 text-xs text-gray-400"> + {{if .DueDate}} + <span class="{{if .IsOverdue}}text-red-400{{end}}">{{.DueDate.Format "Jan 2"}}{{if .HasSetTime}}, {{.DueDate.Format "3:04pm"}}{{end}}</span> + {{end}} + {{if gt .Priority 2}} + <span class="text-red-500 font-medium">P{{.Priority}}</span> + {{end}} </div> </div> </div> - {{end}} - </div> - {{else}} - <div class="bg-gray-50 rounded-lg p-8 text-center"> - <p class="text-gray-600">No upcoming tasks found.</p> </div> {{end}} - </section> + </div> + {{else}} + <div class="bg-white/50 rounded-lg p-6 text-center"> + <p class="text-gray-500 text-sm">No tasks found.</p> + </div> + {{end}} </div> {{end}} |
