diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
index 3ea957c..f4bbc7d 100644
--- a/frontend/public/favicon.svg
+++ b/frontend/public/favicon.svg
@@ -1,40 +1,23 @@
-
diff --git a/frontend/public/icon.svg b/frontend/public/icon.svg
index 2a4bf1b..df78114 100644
--- a/frontend/public/icon.svg
+++ b/frontend/public/icon.svg
@@ -1,43 +1,33 @@
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/ios/TimeTracker/TimeTracker/Features/TimeEntries/TimeEntriesView.swift b/ios/TimeTracker/TimeTracker/Features/TimeEntries/TimeEntriesView.swift
index fdf252f..f9de6cb 100644
--- a/ios/TimeTracker/TimeTracker/Features/TimeEntries/TimeEntriesView.swift
+++ b/ios/TimeTracker/TimeTracker/Features/TimeEntries/TimeEntriesView.swift
@@ -2,27 +2,19 @@ import SwiftUI
struct TimeEntriesView: View {
@StateObject private var viewModel = TimeEntriesViewModel()
- @State private var selectedDay: Date? = Calendar.current.startOfDay(for: Date())
- @State private var visibleWeekStart: Date = Self.mondayOfWeek(containing: Date())
+
+ // dayOffset is the source of truth: 0 = today, -1 = yesterday, etc.
+ @State private var dayOffset: Int = 0
+ // tabSelection is always snapped back to 1 (middle) after each swipe.
+ // Pages are: 0 = dayOffset-1, 1 = dayOffset, 2 = dayOffset+1
+ @State private var tabSelection: Int = 1
+
@State private var showFilterSheet = false
@State private var showAddEntry = false
@State private var entryToEdit: TimeEntry?
@State private var entryToDelete: TimeEntry?
@State private var showDeleteConfirmation = false
- private static func mondayOfWeek(containing date: Date) -> Date {
- var cal = Calendar.current
- cal.firstWeekday = 2 // Monday
- let comps = cal.dateComponents([.yearForWeekOfYear, .weekOfYear], from: date)
- return cal.date(from: comps) ?? Calendar.current.startOfDay(for: date)
- }
-
- private var visibleWeekDays: [Date] {
- (0..<7).compactMap {
- Calendar.current.date(byAdding: .day, value: $0, to: visibleWeekStart)
- }
- }
-
var body: some View {
NavigationStack {
Group {
@@ -37,6 +29,7 @@ struct TimeEntriesView: View {
}
}
.navigationTitle("Entries")
+ .navigationBarTitleDisplayMode(.inline)
.toolbar { toolbarContent }
.task { await viewModel.loadEntries() }
.refreshable { await viewModel.loadEntries() }
@@ -81,32 +74,64 @@ struct TimeEntriesView: View {
Image(systemName: viewModel.activeFilters.isEmpty ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill")
}
}
+
+ // Place the DatePicker in the principal placement (center of nav bar)
+ ToolbarItem(placement: .principal) {
+ DatePicker(
+ "",
+ selection: Binding(
+ get: {
+ Calendar.current.date(byAdding: .day, value: dayOffset, to: Calendar.current.startOfDay(for: Date())) ?? Date()
+ },
+ set: { newDate in
+ let today = Calendar.current.startOfDay(for: Date())
+ let normalizedNewDate = Calendar.current.startOfDay(for: newDate)
+ let components = Calendar.current.dateComponents([.day], from: today, to: normalizedNewDate)
+ if let dayDifference = components.day {
+ dayOffset = dayDifference
+ }
+ }
+ ),
+ displayedComponents: .date
+ )
+ .datePickerStyle(.compact)
+ .labelsHidden()
+ .environment(\.locale, Locale.current) // Ensure correct start of week
+ }
}
// MARK: - Main content
private var mainContent: some View {
- VStack(spacing: 0) {
- WeekStripView(
- weekDays: visibleWeekDays,
- selectedDay: $selectedDay,
- daysWithEntries: viewModel.daysWithEntries,
- onSwipeLeft: {
- visibleWeekStart = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: visibleWeekStart) ?? visibleWeekStart
- },
- onSwipeRight: {
- visibleWeekStart = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: visibleWeekStart) ?? visibleWeekStart
- }
- )
+ // Only 3 pages exist at any time: previous, current, next.
+ // After each swipe settles, we reset tabSelection to 1 and shift
+ // dayOffset, so the carousel appears infinite while staying cheap.
+ TabView(selection: $tabSelection) {
+ ForEach(0..<3, id: \.self) { page in
+ let offset = dayOffset + (page - 1)
+ let day = Calendar.current.date(
+ byAdding: .day,
+ value: offset,
+ to: Calendar.current.startOfDay(for: Date())
+ ) ?? Date()
- Divider()
-
- ScrollView {
- if let day = selectedDay {
+ ScrollView {
dayEntriesSection(for: day)
- } else {
- allEntriesSection
}
+ .tag(page)
+ }
+ }
+ .tabViewStyle(.page(indexDisplayMode: .never))
+ .onChange(of: tabSelection) { _, newPage in
+ guard newPage != 1 else { return }
+ // Shift the logical day offset by how many pages we moved.
+ dayOffset += newPage - 1
+ // Snap back to the middle page without animation so the
+ // surrounding pages are refreshed invisibly.
+ var tx = Transaction()
+ tx.disablesAnimations = true
+ withTransaction(tx) {
+ tabSelection = 1
}
}
}
@@ -116,7 +141,8 @@ struct TimeEntriesView: View {
private func dayEntriesSection(for day: Date) -> some View {
let dayEntries = viewModel.entries(for: day)
return VStack(alignment: .leading, spacing: 0) {
- // Section header
+
+ // Optional: A small summary header for the day
HStack {
Text(dayTitle(day))
.font(.subheadline)
@@ -155,41 +181,7 @@ struct TimeEntriesView: View {
}
}
}
- }
-
- // MARK: - All entries (no day selected) — grouped by day
-
- private var allEntriesSection: some View {
- LazyVStack(alignment: .leading, pinnedViews: .sectionHeaders) {
- ForEach(viewModel.entriesByDay, id: \.date) { group in
- Section {
- ForEach(Array(group.entries.enumerated()), id: \.element.id) { index, entry in
- EntryRow(entry: entry)
- .contentShape(Rectangle())
- .onTapGesture { entryToEdit = entry }
- .swipeActions(edge: .trailing, allowsFullSwipe: false) {
- Button(role: .destructive) {
- entryToDelete = entry
- showDeleteConfirmation = true
- } label: {
- Label("Delete", systemImage: "trash")
- }
- }
- if index < group.entries.count - 1 {
- Divider().padding(.leading, 56)
- }
- }
- } header: {
- Text(dayTitle(group.date))
- .font(.subheadline)
- .fontWeight(.semibold)
- .padding(.horizontal)
- .padding(.vertical, 8)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(.regularMaterial)
- }
- }
- }
+ .padding(.bottom, 40) // Give some breathing room at the bottom of the scroll
}
// MARK: - Helpers
@@ -198,6 +190,7 @@ struct TimeEntriesView: View {
let cal = Calendar.current
if cal.isDateInToday(date) { return "Today" }
if cal.isDateInYesterday(date) { return "Yesterday" }
+ if cal.isDateInTomorrow(date) { return "Tomorrow" }
let formatter = DateFormatter()
formatter.dateFormat = "EEEE, MMM d"
return formatter.string(from: date)
@@ -259,139 +252,6 @@ struct EntryRow: View {
}
}
-// MARK: - Week Strip View
-
-struct WeekStripView: View {
- let weekDays: [Date]
- @Binding var selectedDay: Date?
- let daysWithEntries: Set
- let onSwipeLeft: () -> Void
- let onSwipeRight: () -> Void
-
- @GestureState private var dragOffset: CGFloat = 0
-
- private let cal = Calendar.current
-
- private var monthYearLabel: String {
- // Show the month/year of the majority of days in the strip
- let formatter = DateFormatter()
- formatter.dateFormat = "MMMM yyyy"
- let midWeek = weekDays.count >= 4 ? weekDays[3] : (weekDays.first ?? Date())
- return formatter.string(from: midWeek)
- }
-
- var body: some View {
- VStack(spacing: 4) {
- // Month / year header with navigation arrows
- HStack {
- Button { onSwipeRight() } label: {
- Image(systemName: "chevron.left")
- .font(.footnote.weight(.semibold))
- .foregroundStyle(.secondary)
- .frame(width: 32, height: 32)
- }
- Spacer()
- Text(monthYearLabel)
- .font(.subheadline.weight(.semibold))
- .foregroundStyle(.primary)
- Spacer()
- Button { onSwipeLeft() } label: {
- Image(systemName: "chevron.right")
- .font(.footnote.weight(.semibold))
- .foregroundStyle(.secondary)
- .frame(width: 32, height: 32)
- }
- }
- .padding(.horizontal, 8)
- .padding(.top, 6)
-
- // Day cells
- HStack(spacing: 0) {
- ForEach(weekDays, id: \.self) { day in
- DayCell(
- day: day,
- isSelected: selectedDay.map { cal.isDate($0, inSameDayAs: day) } ?? false,
- isToday: cal.isDateInToday(day),
- hasDot: daysWithEntries.contains(cal.startOfDay(for: day))
- )
- .frame(maxWidth: .infinity)
- .contentShape(Rectangle())
- .onTapGesture {
- let normalized = cal.startOfDay(for: day)
- if let current = selectedDay, cal.isDate(current, inSameDayAs: normalized) {
- selectedDay = nil
- } else {
- selectedDay = normalized
- }
- }
- }
- }
- .padding(.bottom, 6)
- }
- .gesture(
- DragGesture(minimumDistance: 40, coordinateSpace: .local)
- .onEnded { value in
- if value.translation.width < -40 {
- onSwipeLeft()
- } else if value.translation.width > 40 {
- onSwipeRight()
- }
- }
- )
- }
-}
-
-// MARK: - Day Cell
-
-private struct DayCell: View {
- let day: Date
- let isSelected: Bool
- let isToday: Bool
- let hasDot: Bool
-
- private static let weekdayFormatter: DateFormatter = {
- let f = DateFormatter()
- f.dateFormat = "EEEEE" // Single letter: M T W T F S S
- return f
- }()
-
- private static let dayFormatter: DateFormatter = {
- let f = DateFormatter()
- f.dateFormat = "d"
- return f
- }()
-
- var body: some View {
- VStack(spacing: 3) {
- Text(Self.weekdayFormatter.string(from: day))
- .font(.caption2)
- .foregroundStyle(.secondary)
-
- ZStack {
- if isSelected {
- Circle()
- .fill(Color.accentColor)
- .frame(width: 32, height: 32)
- } else if isToday {
- Circle()
- .strokeBorder(Color.accentColor, lineWidth: 1.5)
- .frame(width: 32, height: 32)
- }
-
- Text(Self.dayFormatter.string(from: day))
- .font(.callout.weight(isToday || isSelected ? .semibold : .regular))
- .foregroundStyle(isSelected ? .white : (isToday ? Color.accentColor : .primary))
- }
- .frame(width: 32, height: 32)
-
- // Dot indicator
- Circle()
- .fill(hasDot ? Color.accentColor.opacity(isSelected ? 0 : 0.7) : Color.clear)
- .frame(width: 4, height: 4)
- }
- .padding(.vertical, 4)
- }
-}
// MARK: - Filter Sheet
diff --git a/ios/TimeTracker/TimeTracker/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/TimeTracker/TimeTracker/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
index 13613e3..ddd0a37 100644
--- a/ios/TimeTracker/TimeTracker/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/ios/TimeTracker/TimeTracker/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,6 +1,29 @@
{
"images" : [
{
+ "filename" : "app_icon.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
diff --git a/ios/TimeTracker/TimeTracker/Resources/Assets.xcassets/AppIcon.appiconset/app_icon.png b/ios/TimeTracker/TimeTracker/Resources/Assets.xcassets/AppIcon.appiconset/app_icon.png
new file mode 100644
index 0000000..90987de
Binary files /dev/null and b/ios/TimeTracker/TimeTracker/Resources/Assets.xcassets/AppIcon.appiconset/app_icon.png differ