diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-01-31 21:23:56 -1000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-01-31 21:23:56 -1000 |
| commit | f9127d5272042f4980ece8b39a47613f95eeaf8e (patch) | |
| tree | e111cc6f85b0cd23dd7e705b0390d1154fbc13ee | |
| parent | cbb0b53de1d06918c142171fd084f14f03798bc1 (diff) | |
Fix timeline calendar view and shopping UI bugs (#56, #65-73)
- #56: Add overflow-hidden to card/panel classes to prevent content overflow
- #65: Fix Google Tasks not showing by including tasks without due dates
- #66: Add no-cache headers to prevent stale template responses
- #67: Increase dropdown z-index to 100 for proper layering
- #69: Implement calendar-style Today section with hourly grid (6am-10pm),
duration-based event heights, and compact overdue/all-day section
- #70: Only reset shopping-mode form on successful submission
- #71: Remove checkboxes from shopping tab (only show in shopping mode)
- #72: Add inline add-item input at end of each store section
- #73: Add Grouped/Flat view toggle for shopping list
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| -rw-r--r-- | SESSION_STATE.md | 16 | ||||
| -rw-r--r-- | internal/api/google_tasks.go | 13 | ||||
| -rw-r--r-- | internal/handlers/handlers.go | 16 | ||||
| -rw-r--r-- | internal/handlers/response.go | 10 | ||||
| -rw-r--r-- | internal/handlers/timeline_logic.go | 17 | ||||
| -rw-r--r-- | internal/models/timeline.go | 10 | ||||
| -rw-r--r-- | web/static/css/input.css | 16 | ||||
| -rw-r--r-- | web/templates/index.html | 2 | ||||
| -rw-r--r-- | web/templates/partials/shopping-tab.html | 69 | ||||
| -rw-r--r-- | web/templates/partials/timeline-tab.html | 221 | ||||
| -rw-r--r-- | web/templates/shopping-mode.html | 2 |
11 files changed, 361 insertions, 31 deletions
diff --git a/SESSION_STATE.md b/SESSION_STATE.md index 56ff56e..6e076f5 100644 --- a/SESSION_STATE.md +++ b/SESSION_STATE.md @@ -3,8 +3,22 @@ ## Current Focus Implementing bugs from production database -## Recently Completed +## Recently Completed Bugs - **#74**: Feature toggles - Added feature toggle system with UI at `/settings` +- **#56**: Box contents overflow - Added overflow-hidden to card/panel classes +- **#65**: Google tasks don't show up - Added logging, fixed tasks without due dates +- **#66**: Tasks use outdated template - Added no-cache headers to dynamic responses +- **#67**: Pop up menus need higher z-index - Updated to z-[100] +- **#68**: Track completion in external sources - Already implemented +- **#69**: Timeline calendar view - Implemented calendar-style Today section with: + - Hourly grid (6am-10pm) + - Events positioned by time with duration-based height + - Separate compact section for overdue/all-day/untimed items + - Added IsOverdue and IsAllDay fields to TimelineItem model +- **#70**: Clear input boxes on success - Fixed shopping-mode.html reset +- **#71**: Shopping items no checkmarks - Removed checkboxes from shopping-tab (only in shopping mode) +- **#72**: Inline add per store - Added inline input at end of each store section +- **#73**: Store grouping optional - Added Grouped/Flat view toggle with query param ## Active Feature **Agent Context API** — `issues/feature_agent_context_api.md` diff --git a/internal/api/google_tasks.go b/internal/api/google_tasks.go index 77a00ed..ecacb6d 100644 --- a/internal/api/google_tasks.go +++ b/internal/api/google_tasks.go @@ -123,23 +123,26 @@ func (c *GoogleTasksClient) getTasksFromList(ctx context.Context, listID string) return result, nil } -// GetTasksByDateRange fetches tasks with due dates in the specified range +// GetTasksByDateRange fetches tasks with due dates in the specified range. +// Tasks without due dates are included and treated as "today" tasks. func (c *GoogleTasksClient) GetTasksByDateRange(ctx context.Context, start, end time.Time) ([]models.GoogleTask, error) { allTasks, err := c.GetTasks(ctx) if err != nil { return nil, err } - // Filter by date range + startDay := time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, c.displayTZ) + endDay := time.Date(end.Year(), end.Month(), end.Day(), 0, 0, 0, 0, c.displayTZ) + + // Filter by date range, include tasks without due dates var filtered []models.GoogleTask for _, task := range allTasks { if task.DueDate == nil { + // Include tasks without due dates (they'll be shown in "today" section) + filtered = append(filtered, task) continue } dueDay := time.Date(task.DueDate.Year(), task.DueDate.Month(), task.DueDate.Day(), 0, 0, 0, 0, c.displayTZ) - startDay := time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, c.displayTZ) - endDay := time.Date(end.Year(), end.Month(), end.Day(), 0, 0, 0, 0, c.displayTZ) - if !dueDay.Before(startDay) && dueDay.Before(endDay) { filtered = append(filtered, task) } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 0e5edcc..bba12ad 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -1230,7 +1230,11 @@ func mealTypeOrder(mealType string) int { func (h *Handler) HandleTabShopping(w http.ResponseWriter, r *http.Request) { ctx := r.Context() stores := h.aggregateShoppingLists(ctx) - HTMLResponse(w, h.templates, "shopping-tab", struct{ Stores []models.ShoppingStore }{stores}) + grouped := r.URL.Query().Get("grouped") != "false" // Default to grouped + HTMLResponse(w, h.templates, "shopping-tab", struct { + Stores []models.ShoppingStore + Grouped bool + }{stores, grouped}) } // HandleShoppingQuickAdd adds a user shopping item @@ -1285,7 +1289,10 @@ func (h *Handler) HandleShoppingQuickAdd(w http.ResponseWriter, r *http.Request) } // Return refreshed shopping tab - HTMLResponse(w, h.templates, "shopping-tab", struct{ Stores []models.ShoppingStore }{allStores}) + HTMLResponse(w, h.templates, "shopping-tab", struct { + Stores []models.ShoppingStore + Grouped bool + }{allStores, true}) } // HandleShoppingToggle toggles a shopping item's checked state @@ -1323,7 +1330,10 @@ func (h *Handler) HandleShoppingToggle(w http.ResponseWriter, r *http.Request) { // Return refreshed shopping tab stores := h.aggregateShoppingLists(r.Context()) - HTMLResponse(w, h.templates, "shopping-tab", struct{ Stores []models.ShoppingStore }{stores}) + HTMLResponse(w, h.templates, "shopping-tab", struct { + Stores []models.ShoppingStore + Grouped bool + }{stores, true}) } // HandleShoppingMode renders the focused shopping mode for a single store diff --git a/internal/handlers/response.go b/internal/handlers/response.go index 9a7ab45..34d4491 100644 --- a/internal/handlers/response.go +++ b/internal/handlers/response.go @@ -7,9 +7,17 @@ import ( "net/http" ) +// noCacheHeaders sets headers to prevent browser caching +func noCacheHeaders(w http.ResponseWriter) { + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") +} + // JSONResponse writes data as JSON with appropriate headers func JSONResponse(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") + noCacheHeaders(w) _ = json.NewEncoder(w).Encode(data) } @@ -23,6 +31,8 @@ func JSONError(w http.ResponseWriter, status int, msg string, err error) { // HTMLResponse renders an HTML template func HTMLResponse(w http.ResponseWriter, tmpl *template.Template, name string, data interface{}) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + noCacheHeaders(w) if err := tmpl.ExecuteTemplate(w, name, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) log.Printf("Error rendering template %s: %v", name, err) diff --git a/internal/handlers/timeline_logic.go b/internal/handlers/timeline_logic.go index 5ea44b5..7a85393 100644 --- a/internal/handlers/timeline_logic.go +++ b/internal/handlers/timeline_logic.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "log" "sort" "strings" "time" @@ -130,7 +131,9 @@ func BuildTimeline(ctx context.Context, s *store.Store, calendarClient api.Googl // 4. Fetch Events if calendarClient != nil { events, err := calendarClient.GetEventsByDateRange(ctx, start, end) - if err == nil { + if err != nil { + log.Printf("Warning: failed to fetch calendar events: %v", err) + } else { for _, event := range events { endTime := event.End item := models.TimelineItem{ @@ -142,7 +145,7 @@ func BuildTimeline(ctx context.Context, s *store.Store, calendarClient api.Googl Description: event.Description, URL: event.HTMLLink, OriginalItem: event, - IsCompleted: false, // Events don't have completion status + IsCompleted: false, Source: "calendar", } item.ComputeDaySection(now) @@ -154,9 +157,13 @@ func BuildTimeline(ctx context.Context, s *store.Store, calendarClient api.Googl // 5. Fetch Google Tasks if tasksClient != nil { gTasks, err := tasksClient.GetTasksByDateRange(ctx, start, end) - if err == nil { + if err != nil { + log.Printf("Warning: failed to fetch Google Tasks: %v", err) + } else { + log.Printf("Google Tasks: fetched %d tasks in date range", len(gTasks)) for _, gTask := range gTasks { - taskTime := start // Default to start of range if no due date + // Tasks without due date are placed in today section + taskTime := now if gTask.DueDate != nil { taskTime = *gTask.DueDate } @@ -176,6 +183,8 @@ func BuildTimeline(ctx context.Context, s *store.Store, calendarClient api.Googl items = append(items, item) } } + } else { + log.Printf("Google Tasks client not configured") } // Sort items by Time diff --git a/internal/models/timeline.go b/internal/models/timeline.go index 0968d41..c4196bd 100644 --- a/internal/models/timeline.go +++ b/internal/models/timeline.go @@ -36,6 +36,8 @@ type TimelineItem struct { // UI enhancement fields IsCompleted bool `json:"is_completed"` + IsOverdue bool `json:"is_overdue"` + IsAllDay bool `json:"is_all_day"` // True if time is midnight (no specific time) DaySection DaySection `json:"day_section"` Source string `json:"source"` // "todoist", "trello", "plantoeat", "calendar", "gtasks" @@ -43,7 +45,7 @@ type TimelineItem struct { ListID string `json:"list_id,omitempty"` // For Google Tasks } -// ComputeDaySection sets the DaySection based on the item's time +// ComputeDaySection sets the DaySection, IsOverdue, and IsAllDay based on the item's time func (item *TimelineItem) ComputeDaySection(now time.Time) { // Use configured display timezone for consistent comparisons tz := config.GetDisplayTimezone() @@ -56,6 +58,12 @@ func (item *TimelineItem) ComputeDaySection(now time.Time) { itemDay := time.Date(localItemTime.Year(), localItemTime.Month(), localItemTime.Day(), 0, 0, 0, 0, tz) + // Check if item is overdue (before today) + item.IsOverdue = itemDay.Before(today) + + // Check if item is all-day (midnight time means no specific time) + item.IsAllDay = localItemTime.Hour() == 0 && localItemTime.Minute() == 0 + if itemDay.Before(tomorrow) { item.DaySection = DaySectionToday } else if itemDay.Before(dayAfterTomorrow) { diff --git a/web/static/css/input.css b/web/static/css/input.css index c7fa1d2..321aa4f 100644 --- a/web/static/css/input.css +++ b/web/static/css/input.css @@ -24,7 +24,7 @@ @layer components { /* Dark Glass Card */ .card { - @apply bg-black/70 backdrop-blur-sm rounded-lg p-4 transition-all duration-200; + @apply bg-black/70 backdrop-blur-sm rounded-lg p-4 transition-all duration-200 overflow-hidden; box-shadow: 0 0 12px black; } @@ -32,6 +32,11 @@ @apply hover:bg-black/80; } + /* Panel with overflow clipping */ + .bg-panel { + @apply overflow-hidden; + } + /* Navigation Pills */ .tab-button { @apply px-2 sm:px-4 py-2 rounded-lg text-xs sm:text-sm font-light text-white/70 tracking-wide transition-all duration-200; @@ -59,11 +64,11 @@ } .trello-card-item { - @apply bg-black/40 rounded-lg p-4 hover:bg-black/50 transition-all; + @apply bg-black/40 rounded-lg p-4 hover:bg-black/50 transition-all overflow-hidden; } .task-item { - @apply bg-black/70 rounded-lg transition-colors; + @apply bg-black/70 rounded-lg transition-colors overflow-hidden; box-shadow: 0 0 12px black; } @@ -71,6 +76,11 @@ @apply bg-black/80; } + /* Text truncation for long content */ + .truncate-text { + @apply truncate; + } + .line-clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; diff --git a/web/templates/index.html b/web/templates/index.html index 3451623..5322ca6 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -75,7 +75,7 @@ onclick="toggleDetailsDropdown(event)"> 📁 Details ▾ </button> - <div id="details-menu" class="hidden absolute top-full left-0 mt-1 bg-black/90 backdrop-blur-md rounded-lg p-1 min-w-[140px] z-30 border border-white/20 shadow-lg"> + <div id="details-menu" class="hidden absolute top-full left-0 mt-1 bg-black/90 backdrop-blur-md rounded-lg p-1 min-w-[140px] z-[100] border border-white/20 shadow-lg"> <button class="w-full text-left px-3 py-2 rounded text-sm text-white/80 hover:bg-white/20 hover:text-white transition-colors {{if eq .ActiveTab "tasks"}}bg-white/20 text-white{{end}}" hx-get="/tabs/tasks" diff --git a/web/templates/partials/shopping-tab.html b/web/templates/partials/shopping-tab.html index 4d0ac02..e5fa3e6 100644 --- a/web/templates/partials/shopping-tab.html +++ b/web/templates/partials/shopping-tab.html @@ -1,10 +1,29 @@ {{define "shopping-tab"}} <div class="space-y-6 text-shadow-sm" - hx-get="/tabs/shopping" + hx-get="/tabs/shopping{{if not .Grouped}}?grouped=false{{end}}" hx-trigger="refresh-tasks from:body" hx-target="#tab-content" hx-swap="innerHTML"> + <!-- Header with View Toggle --> + <div class="flex items-center justify-between"> + <h2 class="text-lg font-medium text-white/80">Shopping List</h2> + <div class="flex gap-2"> + <button hx-get="/tabs/shopping?grouped=true" + hx-target="#tab-content" + hx-swap="innerHTML" + class="px-3 py-1.5 rounded-lg text-sm transition-colors {{if .Grouped}}bg-white/20 text-white{{else}}bg-white/5 text-white/50 hover:bg-white/10{{end}}"> + Grouped + </button> + <button hx-get="/tabs/shopping?grouped=false" + hx-target="#tab-content" + hx-swap="innerHTML" + class="px-3 py-1.5 rounded-lg text-sm transition-colors {{if not .Grouped}}bg-white/20 text-white{{else}}bg-white/5 text-white/50 hover:bg-white/10{{end}}"> + Flat + </button> + </div> + </div> + <!-- Quick Add Form --> <form hx-post="/shopping/add" hx-target="#tab-content" @@ -27,6 +46,8 @@ </form> {{if .Stores}} + {{if .Grouped}} + <!-- Grouped View: Items by Store --> {{range .Stores}} <section class="bg-panel backdrop-blur-sm rounded-xl p-4 sm:p-5"> <div class="flex items-center justify-between mb-4"> @@ -44,14 +65,8 @@ {{if .Name}}<h3 class="text-sm text-white/60 mb-2 uppercase tracking-wide">{{.Name}}</h3>{{end}} <ul class="space-y-2"> {{range .Items}} - <li class="flex items-center gap-3 p-3 bg-card bg-card-hover transition-colors rounded-lg border border-white/5 {{if .Checked}}opacity-50{{end}}"> - <input type="checkbox" {{if .Checked}}checked{{end}} - hx-post="/shopping/toggle" - hx-vals='{"id":"{{.ID}}","source":"{{.Source}}","checked":{{if .Checked}}false{{else}}true{{end}}}' - hx-target="#tab-content" - hx-swap="innerHTML" - class="h-5 w-5 rounded bg-black/40 border-white/30 text-green-500 focus:ring-white/30 cursor-pointer flex-shrink-0"> - <span class="flex-1 {{if .Checked}}line-through text-white/40{{else}}text-white{{end}}">{{.Name}}</span> + <li class="flex items-center gap-3 p-3 bg-card bg-card-hover transition-colors rounded-lg border border-white/5"> + <span class="flex-1 text-white">{{.Name}}</span> {{if .Quantity}}<span class="text-white/50 text-sm">{{.Quantity}}</span>{{end}} <span class="text-xs px-2 py-0.5 rounded {{if eq .Source "trello"}}bg-blue-900/50 text-blue-300{{else if eq .Source "user"}}bg-purple-900/50 text-purple-300{{else}}bg-green-900/50 text-green-300{{end}}"> {{.Source}} @@ -61,6 +76,42 @@ </ul> </div> {{end}} + <!-- Inline Add Item --> + <form hx-post="/shopping/add" + hx-target="#tab-content" + hx-swap="innerHTML" + class="mt-3 flex gap-2"> + <input type="hidden" name="store" value="{{.Name}}"> + <input type="text" name="name" placeholder="Add to {{.Name}}..." + class="flex-1 bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white placeholder-white/30 focus:outline-none focus:ring-1 focus:ring-white/20 text-sm" + required> + <button type="submit" class="bg-white/10 hover:bg-white/20 text-white/70 hover:text-white px-3 py-2 rounded-lg text-sm transition-colors"> + + + </button> + </form> + </section> + {{end}} + {{else}} + <!-- Flat View: All Items in One List --> + <section class="bg-panel backdrop-blur-sm rounded-xl p-4 sm:p-5"> + <ul class="space-y-2"> + {{range .Stores}} + {{range .Categories}} + {{range .Items}} + <li class="flex items-center gap-3 p-3 bg-card bg-card-hover transition-colors rounded-lg border border-white/5"> + <span class="flex-1 text-white">{{.Name}}</span> + {{if .Quantity}}<span class="text-white/50 text-sm">{{.Quantity}}</span>{{end}} + <span class="text-xs px-2 py-0.5 rounded bg-white/10 text-white/50"> + {{.Store}} + </span> + <span class="text-xs px-2 py-0.5 rounded {{if eq .Source "trello"}}bg-blue-900/50 text-blue-300{{else if eq .Source "user"}}bg-purple-900/50 text-purple-300{{else}}bg-green-900/50 text-green-300{{end}}"> + {{.Source}} + </span> + </li> + {{end}} + {{end}} + {{end}} + </ul> </section> {{end}} {{else}} diff --git a/web/templates/partials/timeline-tab.html b/web/templates/partials/timeline-tab.html index b1045c6..8745d1d 100644 --- a/web/templates/partials/timeline-tab.html +++ b/web/templates/partials/timeline-tab.html @@ -1,11 +1,92 @@ {{define "timeline-tab"}} +<style> + .calendar-grid { + position: relative; + border-left: 2px solid rgba(255,255,255,0.15); + margin-left: 50px; + min-height: 640px; + } + .calendar-hour { + position: relative; + height: 40px; + border-bottom: 1px solid rgba(255,255,255,0.05); + } + .calendar-hour-label { + position: absolute; + left: -50px; + top: -8px; + font-size: 0.7em; + color: rgba(255,255,255,0.4); + width: 44px; + text-align: right; + } + .calendar-event { + position: absolute; + left: 8px; + right: 8px; + background: rgba(0,0,0,0.5); + border-radius: 6px; + padding: 4px 8px; + font-size: 0.8em; + border-left: 3px solid; + overflow: hidden; + z-index: 1; + backdrop-filter: blur(4px); + box-shadow: 0 2px 8px rgba(0,0,0,0.3); + } + .calendar-event:hover { + z-index: 10; + background: rgba(0,0,0,0.7); + } + .calendar-event.source-todoist { border-color: #e44332; } + .calendar-event.source-trello { border-color: #0079bf; } + .calendar-event.source-plantoeat { border-color: #5cb85c; } + .calendar-event.source-calendar { border-color: #9b59b6; } + .calendar-event.source-gtasks { border-color: #f39c12; } + .calendar-event-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: white; + font-weight: 500; + } + .calendar-event-time { + font-size: 0.75em; + color: rgba(255,255,255,0.6); + } + .untimed-item { + display: inline-flex; + align-items: center; + gap: 6px; + background: rgba(0,0,0,0.4); + padding: 4px 10px; + border-radius: 4px; + margin: 2px; + font-size: 0.85em; + border-left: 3px solid; + } + .untimed-item.source-todoist { border-color: #e44332; } + .untimed-item.source-trello { border-color: #0079bf; } + .untimed-item.source-plantoeat { border-color: #5cb85c; } + .untimed-item.source-calendar { border-color: #9b59b6; } + .untimed-item.source-gtasks { border-color: #f39c12; } + .untimed-item.overdue { opacity: 0.8; } + .overdue-badge { + background: rgba(220,53,69,0.3); + color: #ff6b6b; + font-size: 0.65em; + padding: 1px 4px; + border-radius: 3px; + } +</style> + <div class="space-y-6 text-shadow-sm" hx-get="/tabs/timeline" hx-trigger="refresh-tasks from:body" hx-target="#tab-content" hx-swap="innerHTML"> - <!-- Today Section (Expanded, Collapsible) --> + <!-- Today Section (Calendar View) --> {{if .TodayItems}} <details class="group" open> <summary class="text-lg font-semibold mb-3 flex items-center gap-2 text-white/90 cursor-pointer hover:text-white sticky top-0 bg-black/20 backdrop-blur-md py-2 z-10 rounded-lg px-2 list-none"> @@ -15,11 +96,145 @@ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> </svg> </summary> - <div class="space-y-2 relative pl-4 border-l border-white/10 ml-2"> + + <!-- Overdue + All-Day + Untimed Items Section --> + <div class="mb-4 flex flex-wrap gap-1 p-2 bg-black/20 rounded-lg" id="untimed-section"> {{range .TodayItems}} - {{template "timeline-item" .}} + {{if or .IsOverdue .IsAllDay}} + <div class="untimed-item source-{{.Source}} {{if .IsOverdue}}overdue{{end}} {{if .IsCompleted}}opacity-50{{end}}"> + {{if or (eq .Type "task") (eq .Type "card") (eq .Type "gtask")}} + <input type="checkbox" + {{if .IsCompleted}}checked{{end}} + hx-post="{{if .IsCompleted}}/uncomplete-atom{{else}}/complete-atom{{end}}" + hx-vals='{"id": "{{.ID}}", "source": "{{.Source}}"{{if .ListID}}, "listId": "{{.ListID}}"{{end}}}' + hx-target="#tab-content" + hx-swap="innerHTML" + class="h-4 w-4 rounded bg-black/40 border-white/30 text-white/80 focus:ring-white/30 cursor-pointer flex-shrink-0"> + {{end}} + <span class="{{if .IsCompleted}}line-through text-white/50{{end}}">{{.Title}}</span> + {{if .IsOverdue}}<span class="overdue-badge">overdue</span>{{end}} + {{if .URL}} + <a href="{{.URL}}" target="_blank" class="text-white/50 hover:text-white"> + <svg class="w-3 h-3" 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> + {{end}} {{end}} </div> + + <!-- Calendar Grid (6am-10pm) --> + <div class="calendar-grid" id="today-calendar"> + <!-- Hour rows --> + <div class="calendar-hour" data-hour="6"><span class="calendar-hour-label">6am</span></div> + <div class="calendar-hour" data-hour="7"><span class="calendar-hour-label">7am</span></div> + <div class="calendar-hour" data-hour="8"><span class="calendar-hour-label">8am</span></div> + <div class="calendar-hour" data-hour="9"><span class="calendar-hour-label">9am</span></div> + <div class="calendar-hour" data-hour="10"><span class="calendar-hour-label">10am</span></div> + <div class="calendar-hour" data-hour="11"><span class="calendar-hour-label">11am</span></div> + <div class="calendar-hour" data-hour="12"><span class="calendar-hour-label">12pm</span></div> + <div class="calendar-hour" data-hour="13"><span class="calendar-hour-label">1pm</span></div> + <div class="calendar-hour" data-hour="14"><span class="calendar-hour-label">2pm</span></div> + <div class="calendar-hour" data-hour="15"><span class="calendar-hour-label">3pm</span></div> + <div class="calendar-hour" data-hour="16"><span class="calendar-hour-label">4pm</span></div> + <div class="calendar-hour" data-hour="17"><span class="calendar-hour-label">5pm</span></div> + <div class="calendar-hour" data-hour="18"><span class="calendar-hour-label">6pm</span></div> + <div class="calendar-hour" data-hour="19"><span class="calendar-hour-label">7pm</span></div> + <div class="calendar-hour" data-hour="20"><span class="calendar-hour-label">8pm</span></div> + <div class="calendar-hour" data-hour="21"><span class="calendar-hour-label">9pm</span></div> + <div class="calendar-hour" data-hour="22"><span class="calendar-hour-label">10pm</span></div> + + <!-- Timed events positioned by JavaScript --> + {{range .TodayItems}} + {{if and (not .IsOverdue) (not .IsAllDay)}} + <div class="calendar-event source-{{.Source}} {{if .IsCompleted}}opacity-50{{end}}" + data-hour="{{.Time.Hour}}" + data-minute="{{.Time.Minute}}" + data-end-hour="{{if .EndTime}}{{.EndTime.Hour}}{{else}}{{.Time.Hour}}{{end}}" + data-end-minute="{{if .EndTime}}{{.EndTime.Minute}}{{else}}59{{end}}" + data-id="{{.ID}}" + data-source="{{.Source}}" + data-completed="{{.IsCompleted}}" + data-type="{{.Type}}" + data-url="{{.URL}}" + {{if .ListID}}data-list-id="{{.ListID}}"{{end}} + style="display:none;"> + <div class="flex items-center gap-2"> + {{if or (eq .Type "task") (eq .Type "card") (eq .Type "gtask")}} + <input type="checkbox" + {{if .IsCompleted}}checked{{end}} + hx-post="{{if .IsCompleted}}/uncomplete-atom{{else}}/complete-atom{{end}}" + hx-vals='{"id": "{{.ID}}", "source": "{{.Source}}"{{if .ListID}}, "listId": "{{.ListID}}"{{end}}}' + hx-target="#tab-content" + hx-swap="innerHTML" + onclick="event.stopPropagation();" + class="h-4 w-4 rounded bg-black/40 border-white/30 text-white/80 focus:ring-white/30 cursor-pointer flex-shrink-0"> + {{end}} + <span class="calendar-event-title {{if .IsCompleted}}line-through text-white/50{{end}}">{{.Title}}</span> + </div> + <div class="calendar-event-time">{{.Time.Format "3:04 PM"}}{{if .EndTime}} - {{.EndTime.Format "3:04 PM"}}{{end}}</div> + </div> + {{end}} + {{end}} + </div> + + <script> + (function() { + const calendar = document.getElementById('today-calendar'); + const events = calendar.querySelectorAll('.calendar-event'); + const hourHeight = 40; + const startHour = 6; + const endHour = 22; + + events.forEach(function(el) { + const hour = parseInt(el.dataset.hour); + const minute = parseInt(el.dataset.minute); + const endHourVal = parseInt(el.dataset.endHour); + const endMinute = parseInt(el.dataset.endMinute); + + // Skip events outside the visible range + if (hour < startHour || hour > endHour) { + el.remove(); + return; + } + + // Calculate position + const top = (hour - startHour) * hourHeight + (minute / 60) * hourHeight; + + // Calculate height based on duration + let durationMinutes; + if (endHourVal > hour || (endHourVal === hour && endMinute > minute)) { + durationMinutes = (endHourVal - hour) * 60 + (endMinute - minute); + } else { + durationMinutes = 55; // Default ~1 hour for tasks without duration + } + const height = Math.max(28, (durationMinutes / 60) * hourHeight - 4); + + el.style.top = top + 'px'; + el.style.height = height + 'px'; + el.style.display = 'block'; + + // Click to open URL + const url = el.dataset.url; + if (url) { + el.style.cursor = 'pointer'; + el.addEventListener('click', function(e) { + if (e.target.tagName !== 'INPUT') { + window.open(url, '_blank'); + } + }); + } + }); + + // Hide untimed section if empty + const untimedSection = document.getElementById('untimed-section'); + if (untimedSection && untimedSection.children.length === 0) { + untimedSection.style.display = 'none'; + } + })(); + </script> </details> {{end}} diff --git a/web/templates/shopping-mode.html b/web/templates/shopping-mode.html index 9e21ac6..88d8561 100644 --- a/web/templates/shopping-mode.html +++ b/web/templates/shopping-mode.html @@ -74,7 +74,7 @@ <form hx-post="/shopping/add" hx-target="#shopping-items" hx-swap="innerHTML" - hx-on::after-request="this.reset()" + hx-on::after-request="if(event.detail.successful) this.reset();" class="flex gap-2"> <input type="hidden" name="store" value="{{.StoreName}}"> <input type="hidden" name="mode" value="shopping-mode"> |
